[
  {
    "path": ".gitignore",
    "content": "build\n.gradle\n.idea\nlocal.properties\napp/app.iml\nplayerlib/playerlib.iml\nexoplayerlib/exoplayerlib.iml\nijkplayerlib/ijkplayerlib.iml\nmediaproxylib/mediaproxylib.iml\nmediaproxylib/build\nmediaproxylib/build/*\n/MediaSDK.iml"
  },
  {
    "path": "LICENSE",
    "content": "                                 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 2014-2016 Alexey Danilov\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": "# MediaSDK\n\nThe library is working for downloading video while playing the video, the video contains M3U8/MP4 <br>\n\nDeveloper documentation is here, [Click it](./README_cn.md)<br>\n\nYou can refer to the technical documentation：https://www.jianshu.com/p/27085da32a35 <br>\n\nUse the library: <br>\n```\nallprojects {\n  repositories {\n    ...\n    maven { url 'https://jitpack.io' }\n  }\n}\n\ndependencies {\n        implementation 'com.github.JeffMony:MediaSDK:2.0.0'\n}\n```\n\n#### The Core functions of the project\n> * Cache management, LRU\n> * Video downloading management\n> * Local proxy management\n> * Display downloading speed\n> * Display the video cache's size\n> * Support the video's downloading while playing the video, M3U8/MP4 video\n\nThe project's architecture is as follows：\n![](./files/LocalProxy.png)\n\n\n### Developer documentation\n#### 1.Application->onCreate(...)\n```\nFile file = LocalProxyUtils.getVideoCacheDir(this);\nif (!file.exists()) {\n    file.mkdir();\n}\nLocalProxyConfig config = new VideoDownloadManager.Build(this)\n    .setCacheRoot(file)\n    .setUrlRedirect(false)\n    .setTimeOut(DownloadConstants.READ_TIMEOUT, DownloadConstants.CONN_TIMEOUT, DownloadConstants.SOCKET_TIMEOUT)\n    .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT)\n    .setIgnoreAllCertErrors(true)\n    .buildConfig();\nVideoDownloadManager.getInstance().initConfig(config);\n```\n1.setCacheRoot            The cache path；\n2.setUrlRedirect          Support request's redirect；\n3.setCacheSize            Set cache size；\n4.setTimeOut              Set request's timeout；\n5.setPort                 Set the local proxy server's port；\n6.setIgnoreAllCertErrors  Support the certificate；\n#### 2.The local proxy server's switch\n```\nPlayerAttributes attributes = new PlayerAttributes();\nattributes.setUseLocalProxy(mUseLocalProxy);\n```\n#### 3.Set the listener\n```\nmPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener);\nmPlayer.startLocalProxy(mUrl, null);\n\nprivate IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() {\n    @Override\n    public void onCacheReady(IPlayer mp, String proxyUrl) {\n        LogUtils.w(\"onCacheReady proxyUrl = \" + proxyUrl);\n        Uri uri = Uri.parse(proxyUrl);\n        try {\n            mPlayer.setDataSource(PlayerActivity.this, uri);\n        } catch (IOException e) {\n            e.printStackTrace();\n            return;\n        }\n        mPlayer.setSurface(mSurface);\n        mPlayer.setOnPreparedListener(mPreparedListener);\n        mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener);\n        mPlayer.prepareAsync();\n    }\n\n    @Override\n    public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) {\n        LogUtils.w(\"onCacheProgressChanged percent = \" + percent);\n        mPercent = percent;\n    }\n\n    @Override\n    public void onCacheSpeedChanged(String url, float cacheSpeed) {\n        if (mPlayer != null && mPlayer.get() != null) {\n            mPlayer.get().notifyProxyCacheSpeed(cacheSpeed);\n        }\n    }\n\n    @Override\n    public void onCacheFinished(String url) {\n        LogUtils.i(\"onCacheFinished url=\"+url + \", player=\"+this);\n        mIsCompleteCached = true;\n    }\n\n    @Override\n    public void onCacheForbidden(String url) {\n        LogUtils.w(\"onCacheForbidden url=\"+url+\", player=\"+this);\n        mUseLocalProxy = false;\n        if (mPlayer != null && mPlayer.get() != null) {\n            mPlayer.get().notifyProxyCacheForbidden(url);\n        }\n    }\n\n    @Override\n    public void onCacheFailed(String url, Exception e) {\n        LogUtils.w(\"onCacheFailed , player=\"+this);\n        pauseProxyCacheTask(PROXY_CACHE_EXCEPTION);\n    }\n};\n```\n\ndemo：<br>\n![](./files/test1_low.jpg)![](./files/test2_low.jpg)\n\n![](./files/JeffMony.jpg)\n\n![](./files/ErWeiMa.jpg)\n"
  },
  {
    "path": "README_cn.md",
    "content": "# MediaSDK\n\n关注一下分析文章：https://www.jianshu.com/p/27085da32a35\n\n```\nallprojects {\n  repositories {\n    ...\n    maven { url 'https://jitpack.io' }\n  }\n}\n\ndependencies {\n        implementation 'com.github.JeffMony:MediaSDK:2.0.0'\n}\n```\n\n最近这个项目有新的维护计划：\n> * 1.本地代理的控制逻辑移到server端\n> * 2.增加mp4 moov端的识别规则\n> * 3.将本地代理库和播放器解耦\n\n### 版本LOG\n2.0.0\n> * 1.使用androidasync替换proxyserver\n> *  2.优化MediaSDK接口\n\nt1.5.0\n> * 1.视频下载队列，可以设置视频并发下载的个数\n> * 2.视频播放缓存和下载缓存的数据合并，但是逻辑分离\n\nt1.4.0\n> * 1.增加视频下载模块;\n> * 2.重构本地代理模块代码;\n> * 3.视频下载和本地代理模块代码复用;\n> * 4.还有一些bug待处理,很快更新\n> * 5.后续版本更新计划: 下载队列；初始化本地已下载的视频；下载和播放缓存隔离；\n\nt1.3.0\n> * 1.封装好边下边播模块\n> * 2.可以直接商用\n\nv1.1.0\n> * 1.解决https 证书出错的视频url请求，信任证书；\n> * 2.解决播放过程中息屏的问题，保持屏幕常亮；\n> * 3.增加 isPlaying 接口，表示当前是否正在播放视频；\n> * 4.解决Cleartext HTTP traffic to 127.0.0.1 not permitted 问题，Android P版本不允许未加密请求；\n\nv1.0.0\n> * 1.支持MediaPlayer/IjkPlayer/ExoPlayer 三种播放器播放视频；\n> * 2.支持M3U8/MP4视频的边下边播功能；\n> * 3.本地代理实现边下边播功能；\n> * 4.提供当前下载速度和下载进度的回调；\n\n\n#### 封装了一个播放器功能库\n> * 实现ijkplayer  exoplayer mediaplayer 3种播放器类型；可以任意切换；\n> * ijkplayer 是从 k0.8.8分支上拉出来的；\n> * exoplayer 是 2.11.1版本\n#### 实现视频边下边播的功能\n> * 缓存管理\n> * 下载管理\n> * 本地代理管理模块(使用androidasync管理本地代理)\n> * 回调播放下载实时速度\n> * 显示缓存大小\n\n本项目的架构如下：\n![](./files/LocalProxy.png)\n从上面的架构可以看出来，本项目的重点在本地代理层，这是实现边下边播的核心逻辑；\n\n\n### 接入库须知\n#### 1.在Application->onCreate(...) 中初始化\n```\nFile file = LocalProxyUtils.getVideoCacheDir(this);\nif (!file.exists()) {\n    file.mkdir();\n}\nLocalProxyConfig config = new VideoDownloadManager.Build(this)\n    .setCacheRoot(file)\n    .setUrlRedirect(false)\n    .setTimeOut(DownloadConstants.READ_TIMEOUT, DownloadConstants.CONN_TIMEOUT, DownloadConstants.SOCKET_TIMEOUT)\n    .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT)\n    .setIgnoreAllCertErrors(true)\n    .buildConfig();\nVideoDownloadManager.getInstance().initConfig(config);\n```\n这儿可以设置一些属性：\n1.setCacheRoot            设置缓存的路径；\n2.setUrlRedirect          是否需要重定向请求；\n3.setCacheSize            设置缓存的大小限制；\n4.setTimeOut              设置连接和读超时时间；\n5.setPort                 设置本地代理的端口；\n6.setIgnoreAllCertErrors  是否需要信任证书；\n#### 2.打开本地代理开关\n```\nPlayerAttributes attributes = new PlayerAttributes();\nattributes.setUseLocalProxy(mUseLocalProxy);\n```\n#### 3.设置本地代理模块监听\n```\nmPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener);\nmPlayer.startLocalProxy(mUrl, null);\n\nprivate IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() {\n    @Override\n    public void onCacheReady(IPlayer mp, String proxyUrl) {\n        LogUtils.w(\"onCacheReady proxyUrl = \" + proxyUrl);\n        Uri uri = Uri.parse(proxyUrl);\n        try {\n            mPlayer.setDataSource(PlayerActivity.this, uri);\n        } catch (IOException e) {\n            e.printStackTrace();\n            return;\n        }\n        mPlayer.setSurface(mSurface);\n        mPlayer.setOnPreparedListener(mPreparedListener);\n        mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener);\n        mPlayer.prepareAsync();\n    }\n\n    @Override\n    public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) {\n        LogUtils.w(\"onCacheProgressChanged percent = \" + percent);\n        mPercent = percent;\n    }\n\n    @Override\n    public void onCacheSpeedChanged(String url, float cacheSpeed) {\n        if (mPlayer != null && mPlayer.get() != null) {\n            mPlayer.get().notifyProxyCacheSpeed(cacheSpeed);\n        }\n    }\n\n    @Override\n    public void onCacheFinished(String url) {\n        LogUtils.i(\"onCacheFinished url=\"+url + \", player=\"+this);\n        mIsCompleteCached = true;\n    }\n\n    @Override\n    public void onCacheForbidden(String url) {\n        LogUtils.w(\"onCacheForbidden url=\"+url+\", player=\"+this);\n        mUseLocalProxy = false;\n        if (mPlayer != null && mPlayer.get() != null) {\n            mPlayer.get().notifyProxyCacheForbidden(url);\n        }\n    }\n\n    @Override\n    public void onCacheFailed(String url, Exception e) {\n        LogUtils.w(\"onCacheFailed , player=\"+this);\n        pauseProxyCacheTask(PROXY_CACHE_EXCEPTION);\n    }\n};\n```\n#### 4.本地代理的生命周期跟着播放器的生命周期一起\n#### 5.下载接入函数\n\n```\npublic interface IDownloadListener {\n\n    void onDownloadPrepare(VideoTaskItem item);\n\n    void onDownloadPending(VideoTaskItem item);\n\n    void onDownloadStart(VideoTaskItem item);\n\n    void onDownloadProxyReady(VideoTaskItem item);\n\n    void onDownloadProgress(VideoTaskItem item);\n\n    void onDownloadSpeed(VideoTaskItem item);\n\n    void onDownloadPause(VideoTaskItem item);\n\n    void onDownloadError(VideoTaskItem item);\n\n    void onDownloadProxyForbidden(VideoTaskItem item);\n\n    void onDownloadSuccess(VideoTaskItem item);\n}\n```\n\n\n### 功能概要\n#### 1.封装了一个player sdk层\n> * 1.1 接入Android 原生的 MediaPlayer 播放器\n> * 1.2 接入google的EXO player 播放器\n> * 1.3 接入开源的 ijk player 播放器\n#### 2.实现整视频的边下边播\n> * 2.1 实现整视频的分片下载\n> * 2.2 实现整视频的断点下载\n#### 3.实现HLS分片视频的边下边播\n> * 3.1 实现HLS视频源的解析工作\n> * 3.2 实现HLS的边下边播\n> * 3.3 实现HLS的断点下载功能\n#### 4.线程池控制下载功能\n#### 5.提供视频下载的额外功能\n> * 5.1 可以提供播放视频或者下载视频的实时网速\n> * 5.2 可以提供已缓存视频的大小\n\ndemo示意图：<br>\n![](./files/test1_low.jpg)![](./files/test2_low.jpg)\n\n欢迎关注我的公众号JeffMony，我会持续为你带来音视频---算法---Android---python 方面的知识分享<br>\n![](./files/JeffMony.jpg)\n\n如果你觉得这个库有用,请鼓励一下吧;<br>\n![](./files/ErWeiMa.jpg)\n"
  },
  {
    "path": "androidasync/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "androidasync/androidasync.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":androidasync\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":androidasync\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_8\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 29 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"Gradle: org.bouncycastle:bcpkix-jdk15on:1.60@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: org.bouncycastle:bcprov-jdk15on:1.60@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.collection:collection:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.annotation:annotation:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.fragment:fragment:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.viewpager:viewpager:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.loader:loader:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.activity:activity:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.customview:customview:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.core:core:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.savedstate:savedstate:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.interpolator:interpolator:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-runtime:2.0.0@aar\" level=\"project\" />\n  </component>\n</module>"
  },
  {
    "path": "androidasync/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion \"29.0.3\"\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    compileOnly group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.60'\n    compileOnly group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.60'\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n}\n"
  },
  {
    "path": "androidasync/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "androidasync/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "androidasync/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.jeffmony.async\" />\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncDatagramSocket.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\n\npublic class AsyncDatagramSocket extends AsyncNetworkSocket {\n    public void disconnect() throws IOException {\n        socketAddress = null;\n        ((DatagramChannelWrapper)getChannel()).disconnect();\n    }\n\n    @Override\n    public InetSocketAddress getRemoteAddress() {\n        if (isOpen())\n            return super.getRemoteAddress();\n        return ((DatagramChannelWrapper)getChannel()).getRemoteAddress();\n    }\n\n    public void connect(InetSocketAddress address) throws IOException {\n        socketAddress = address;\n        ((DatagramChannelWrapper)getChannel()).mChannel.connect(address);\n    }\n\n    public void send(final String host, final int port, final ByteBuffer buffer) {\n        if (getServer().getAffinity() != Thread.currentThread()) {\n            getServer().run(new Runnable() {\n                @Override\n                public void run() {\n                    send(host, port, buffer);\n                }\n            });\n            return;\n        }\n\n        try {\n            ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(host, port));\n        }\n        catch (IOException e) {\n//            close();\n//            reportEndPending(e);\n//            reportClose(e);\n        }\n\n    }\n    public void send(final InetSocketAddress address, final ByteBuffer buffer) {\n        if (getServer().getAffinity() != Thread.currentThread()) {\n            getServer().run(new Runnable() {\n                @Override\n                public void run() {\n                    send(address, buffer);\n                }\n            });\n            return;\n        }\n\n        try {\n            int sent = ((DatagramChannelWrapper)getChannel()).mChannel.send(buffer, new InetSocketAddress(address.getHostName(), address.getPort()));\n        }\n        catch (IOException e) {\n//            Log.e(\"SEND\", \"send error\", e);\n//            close();\n//            reportEndPending(e);\n//            reportClose(e);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncNetworkSocket.java",
    "content": "package com.jeffmony.async;\n\nimport android.util.Log;\n\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.util.Allocator;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.CancelledKeyException;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.SocketChannel;\n\npublic class AsyncNetworkSocket implements AsyncSocket {\n    AsyncNetworkSocket() {\n    }\n\n    @Override\n    public void end() {\n        mChannel.shutdownOutput();\n    }\n\n    public boolean isChunked() {\n        return mChannel.isChunked();\n    }\n\n    InetSocketAddress socketAddress;\n    void attach(SocketChannel channel, InetSocketAddress socketAddress) throws IOException {\n        this.socketAddress = socketAddress;\n        allocator = new Allocator();\n        mChannel = new SocketChannelWrapper(channel);\n    }\n    \n    void attach(DatagramChannel channel) throws IOException {\n        mChannel = new DatagramChannelWrapper(channel);\n        // keep udp at roughly the mtu, which is 1540 or something\n        // letting it grow freaks out nio apparently.\n        allocator = new Allocator(8192);\n    }\n    \n    ChannelWrapper getChannel() {\n        return mChannel;\n    }\n    \n    public void onDataWritable() {\n//        assert mWriteableHandler != null;\n        if (!mChannel.isChunked()) {\n            // turn write off\n            mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps());\n        }\n        if (mWriteableHandler != null)\n            mWriteableHandler.onWriteable();\n    }\n\n    private ChannelWrapper mChannel;\n    private SelectionKey mKey;\n    private AsyncServer mServer;\n    \n    void setup(AsyncServer server, SelectionKey key) {\n        mServer = server;\n        mKey = key;\n    }\n    \n    @Override\n    public void write(final ByteBufferList list) {\n        if (mServer.getAffinity() != Thread.currentThread()) {\n            mServer.run(new Runnable() {\n                @Override\n                public void run() {\n                    write(list);\n                }\n            });\n            return;\n        }\n        if (!mChannel.isConnected()) {\n            assert !mChannel.isChunked();\n            return;\n        }\n\n        try {\n            int before = list.remaining();\n            ByteBuffer[] arr = list.getAllArray();\n            mChannel.write(arr);\n            list.addAll(arr);\n            handleRemaining(list.remaining());\n            mServer.onDataSent(before - list.remaining());\n        }\n        catch (IOException e) {\n            closeInternal();\n            reportEndPending(e);\n            reportClose(e);\n        }\n    }\n    \n    private void handleRemaining(int remaining) throws IOException {\n        if (!mKey.isValid())\n            throw new IOException(new CancelledKeyException());\n        if (remaining > 0) {\n            // chunked channels should not fail\n            assert !mChannel.isChunked();\n            // register for a write notification if a write fails\n            // turn write on\n            mKey.interestOps(SelectionKey.OP_WRITE | mKey.interestOps());\n        }\n        else {\n            // turn write off\n            mKey.interestOps(~SelectionKey.OP_WRITE & mKey.interestOps());\n        }\n    }\n    private ByteBufferList pending = new ByteBufferList();\n//    private ByteBuffer[] buffers = new ByteBuffer[8];\n\n    Allocator allocator;\n    int onReadable() {\n        spitPending();\n        // even if the socket is paused,\n        // it may end up getting a queued readable event if it is\n        // already in the selector's ready queue.\n        if (mPaused)\n            return 0;\n        int total = 0;\n        boolean closed = false;\n\n//            ByteBufferList.obtainArray(buffers, Math.min(Math.max(mToAlloc, 2 << 11), maxAlloc));\n        ByteBuffer b = allocator.allocate();\n        // keep track of the max mount read during this read cycle\n        // so we can be quicker about allocations during the next\n        // time this socket reads.\n        long read;\n        try {\n            read = mChannel.read(b);\n        }\n        catch (Exception e) {\n            read = -1;\n            closeInternal();\n            reportEndPending(e);\n            reportClose(e);\n        }\n\n        if (read < 0) {\n            closeInternal();\n            closed = true;\n        }\n        else {\n            total += read;\n        }\n        if (read > 0) {\n            allocator.track(read);\n            b.flip();\n//                for (int i = 0; i < buffers.length; i++) {\n//                    ByteBuffer b = buffers[i];\n//                    buffers[i] = null;\n//                    b.flip();\n//                    pending.add(b);\n//                }\n            pending.add(b);\n            Util.emitAllData(this, pending);\n        }\n        else {\n            ByteBufferList.reclaim(b);\n        }\n\n        if (closed) {\n            reportEndPending(null);\n            reportClose(null);\n        }\n\n        return total;\n    }\n    \n    boolean closeReported;\n    protected void reportClose(Exception e) {\n        if (closeReported)\n            return;\n        closeReported = true;\n        if (mClosedHander != null) {\n            mClosedHander.onCompleted(e);\n            mClosedHander = null;\n        }\n    }\n\n    @Override\n    public void close() {\n        closeInternal();\n        reportClose(null);\n    }\n\n    private void closeInternal() {\n        mKey.cancel();\n        try {\n            mChannel.close();\n        }\n        catch (IOException e) {\n        }\n    }\n\n    WritableCallback mWriteableHandler;\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        mWriteableHandler = handler;        \n    }\n\n    DataCallback mDataHandler;\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        mDataHandler = callback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return mDataHandler;\n    }\n\n    CompletedCallback mClosedHander;\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        mClosedHander = handler;       \n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        return mClosedHander;\n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        return mWriteableHandler;\n    }\n\n    void reportEnd(Exception e) {\n        if (mEndReported)\n            return;\n        mEndReported = true;\n        if (mCompletedCallback != null)\n            mCompletedCallback.onCompleted(e);\n        else if (e != null) {\n            Log.e(\"NIO\", \"Unhandled exception\", e);\n        }\n    }\n    boolean mEndReported;\n    Exception mPendingEndException;\n    void reportEndPending(Exception e) {\n        if (pending.hasRemaining()) {\n            mPendingEndException = e;\n            return;\n        }\n        reportEnd(e);\n    }\n    \n    private CompletedCallback mCompletedCallback;\n    @Override\n    public void setEndCallback(CompletedCallback callback) {\n        mCompletedCallback = callback;\n    }\n\n    @Override\n    public CompletedCallback getEndCallback() {\n        return mCompletedCallback;\n    }\n\n    @Override\n    public boolean isOpen() {\n        return mChannel.isConnected() && mKey.isValid();\n    }\n    \n    boolean mPaused = false;\n    @Override\n    public void pause() {\n        if (mServer.getAffinity() != Thread.currentThread()) {\n            mServer.run(new Runnable() {\n                @Override\n                public void run() {\n                    pause();\n                }\n            });\n            return;\n        }\n        \n        if (mPaused)\n            return;\n\n        mPaused = true;\n        try {\n            mKey.interestOps(~SelectionKey.OP_READ & mKey.interestOps());\n        }\n        catch (Exception ex) {\n        }\n    }\n    \n    private void spitPending() {\n        if (pending.hasRemaining()) {\n            Util.emitAllData(this, pending);\n        }\n    }\n    \n    @Override\n    public void resume() {\n        if (mServer.getAffinity() != Thread.currentThread()) {\n            mServer.run(new Runnable() {\n                @Override\n                public void run() {\n                    resume();\n                }\n            });\n            return;\n        }\n        \n        if (!mPaused)\n            return;\n        mPaused = false;\n        try {\n            mKey.interestOps(SelectionKey.OP_READ | mKey.interestOps());\n        }\n        catch (Exception ex) {\n        }\n        spitPending();\n        if (!isOpen())\n            reportEndPending(mPendingEndException);\n    }\n    \n    @Override\n    public boolean isPaused() {\n        return mPaused;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mServer;\n    }\n\n\n    public InetSocketAddress getRemoteAddress() {\n        return socketAddress;\n    }\n\n    public InetAddress getLocalAddress() {\n        return mChannel.getLocalAddress();\n    }\n\n    public int getLocalPort() {\n        return mChannel.getLocalPort();\n    }\n\n    public Object getSocket() {\n        return getChannel().getSocket();\n    }\n\n    @Override\n    public String charset() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSSLException.java",
    "content": "package com.jeffmony.async;\n\npublic class AsyncSSLException extends Exception {\n    public AsyncSSLException(Throwable cause) {\n        super(\"Peer not trusted by any of the system trust managers.\", cause);\n    }\n    private boolean mIgnore = false;\n    public void setIgnore(boolean ignore) {\n        mIgnore = ignore;\n    }\n    \n    public boolean getIgnore() {\n        return mIgnore;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSSLServerSocket.java",
    "content": "package com.jeffmony.async;\n\nimport java.security.PrivateKey;\nimport java.security.cert.Certificate;\n\npublic interface AsyncSSLServerSocket extends AsyncServerSocket {\n    PrivateKey getPrivateKey();\n    Certificate getCertificate();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSSLSocket.java",
    "content": "package com.jeffmony.async;\n\nimport java.security.cert.X509Certificate;\n\nimport javax.net.ssl.SSLEngine;\n\npublic interface AsyncSSLSocket extends AsyncSocket {\n    X509Certificate[] getPeerCertificates();\n    SSLEngine getSSLEngine();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSSLSocketWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.Base64;\nimport android.util.Pair;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.callback.ListenCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.SimpleCancellable;\nimport com.jeffmony.async.http.SSLEngineSNIConfigurator;\nimport com.jeffmony.async.util.Allocator;\nimport com.jeffmony.async.util.StreamUtility;\nimport com.jeffmony.async.wrapper.AsyncSocketWrapper;\n\nimport org.apache.http.conn.ssl.StrictHostnameVerifier;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x509.BasicConstraints;\nimport org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;\nimport org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.operator.ContentSigner;\nimport org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.math.BigInteger;\nimport java.net.InetAddress;\nimport java.nio.ByteBuffer;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.KeyStore;\nimport java.security.PrivateKey;\nimport java.security.Provider;\nimport java.security.Security;\nimport java.security.cert.Certificate;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLEngine;\nimport javax.net.ssl.SSLEngineResult;\nimport javax.net.ssl.SSLEngineResult.HandshakeStatus;\nimport javax.net.ssl.SSLEngineResult.Status;\nimport javax.net.ssl.SSLException;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.TrustManagerFactory;\nimport javax.net.ssl.X509TrustManager;\n\npublic class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket {\n    private static final String LOGTAG = \"AsyncSSLSocketWrapper\";\n\n    public interface HandshakeCallback {\n        public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket);\n    }\n\n    static SSLContext defaultSSLContext;\n    static SSLContext trustAllSSLContext;\n    static TrustManager[] trustAllManagers;\n    static HostnameVerifier trustAllVerifier;\n\n    AsyncSocket mSocket;\n    BufferedDataSink mSink;\n    boolean mUnwrapping;\n    SSLEngine engine;\n    boolean finishedHandshake;\n    private int mPort;\n    private String mHost;\n    private boolean mWrapping;\n    HostnameVerifier hostnameVerifier;\n    HandshakeCallback handshakeCallback;\n    X509Certificate[] peerCertificates;\n    WritableCallback mWriteableCallback;\n    DataCallback mDataCallback;\n    TrustManager[] trustManagers;\n    boolean clientMode;\n\n    static {\n        // following is the \"trust the system\" certs setup\n        try {\n            // critical extension 2.5.29.15 is implemented improperly prior to 4.0.3.\n            // https://code.google.com/p/android/issues/detail?id=9307\n            // https://groups.google.com/forum/?fromgroups=#!topic/netty/UCfqPPk5O4s\n            // certs that use this extension will throw in Cipher.java.\n            // fallback is to use a custom SSLContext, and hack around the x509 extension.\n            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)\n                throw new Exception();\n            defaultSSLContext = SSLContext.getInstance(\"Default\");\n        }\n        catch (Exception ex) {\n            try {\n                defaultSSLContext = SSLContext.getInstance(\"TLS\");\n                TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {\n                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {\n                        return new X509Certificate[0];\n                    }\n\n                    public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {\n                    }\n\n                    public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {\n                        for (X509Certificate cert : certs) {\n                            if (cert != null && cert.getCriticalExtensionOIDs() != null)\n                                cert.getCriticalExtensionOIDs().remove(\"2.5.29.15\");\n                        }\n                    }\n                } };\n                defaultSSLContext.init(null, trustAllCerts, null);\n            }\n            catch (Exception ex2) {\n                ex.printStackTrace();\n                ex2.printStackTrace();\n            }\n        }\n\n\n        try {\n            trustAllSSLContext = SSLContext.getInstance(\"TLS\");\n            trustAllManagers = new TrustManager[] { new X509TrustManager() {\n                public java.security.cert.X509Certificate[] getAcceptedIssuers() {\n                    return new X509Certificate[0];\n                }\n\n                public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {\n                }\n\n                public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {\n                }\n            } };\n            trustAllSSLContext.init(null, trustAllManagers, null);\n            trustAllVerifier = (hostname, session) -> true;\n        }\n        catch (Exception ex2) {\n            ex2.printStackTrace();\n        }\n    }\n\n    public static SSLContext getDefaultSSLContext() {\n        return defaultSSLContext;\n    }\n\n    public static void handshake(AsyncSocket socket,\n                                 String host, int port,\n                                 SSLEngine sslEngine,\n                                 TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode,\n                                 final HandshakeCallback callback) {\n        AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode);\n        wrapper.handshakeCallback = callback;\n        socket.setClosedCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (ex != null)\n                    callback.onHandshakeCompleted(ex, null);\n                else\n                    callback.onHandshakeCompleted(new SSLException(\"socket closed during handshake\"), null);\n            }\n        });\n        try {\n            wrapper.engine.beginHandshake();\n            wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus());\n        } catch (SSLException e) {\n            wrapper.report(e);\n        }\n    }\n\n    public static Cancellable connectSocket(AsyncServer server, String host, int port, ConnectCallback callback) {\n        return connectSocket(server, host, port, false, callback);\n    }\n    public static Cancellable connectSocket(AsyncServer server, String host, int port, boolean trustAllCerts, ConnectCallback callback) {\n        SimpleCancellable cancellable = new SimpleCancellable();\n        Cancellable connect = server.connectSocket(host, port, (ex, netSocket) -> {\n            if (ex != null) {\n                if (cancellable.setComplete())\n                    callback.onConnectCompleted(ex, null);\n                return;\n            }\n\n            handshake(netSocket, host, port,\n                    (trustAllCerts ? trustAllSSLContext : defaultSSLContext).createSSLEngine(host, port),\n                    trustAllCerts ? trustAllManagers : null,\n                    trustAllCerts ? trustAllVerifier : null,\n                    true, (e, socket) -> {\n                if (!cancellable.setComplete()) {\n                    if (socket != null)\n                        socket.close();\n                    return;\n                }\n\n                if (e != null)\n                    callback.onConnectCompleted(e, null);\n                else\n                    callback.onConnectCompleted(null, socket);\n            });\n        });\n\n        cancellable.setParent(connect);\n        return cancellable;\n    }\n\n    boolean mEnded;\n    Exception mEndException;\n    final ByteBufferList pending = new ByteBufferList();\n\n    private AsyncSSLSocketWrapper(AsyncSocket socket,\n                                  String host, int port,\n                                  SSLEngine sslEngine,\n                                  TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode) {\n        mSocket = socket;\n        hostnameVerifier = verifier;\n        this.clientMode = clientMode;\n        this.trustManagers = trustManagers;\n        this.engine = sslEngine;\n\n        mHost = host;\n        mPort = port;\n        engine.setUseClientMode(clientMode);\n        mSink = new BufferedDataSink(socket);\n        mSink.setWriteableCallback(new WritableCallback() {\n            @Override\n            public void onWriteable() {\n                if (mWriteableCallback != null)\n                    mWriteableCallback.onWriteable();\n            }\n        });\n\n        // on pause, the emitter is paused to prevent the buffered\n        // socket and itself from firing.\n        // on resume, emitter is resumed, ssl buffer is flushed as well\n        mSocket.setEndCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (mEnded)\n                    return;\n                mEnded = true;\n                mEndException = ex;\n                if (!pending.hasRemaining() && mEndCallback != null)\n                    mEndCallback.onCompleted(ex);\n            }\n        });\n\n        mSocket.setDataCallback(dataCallback);\n    }\n\n    final DataCallback dataCallback = new DataCallback() {\n        final Allocator allocator = new Allocator().setMinAlloc(8192);\n        final ByteBufferList buffered = new ByteBufferList();\n\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            if (mUnwrapping)\n                return;\n            try {\n                mUnwrapping = true;\n\n                bb.get(buffered);\n\n                if (buffered.hasRemaining()) {\n                    ByteBuffer all = buffered.getAll();\n                    buffered.add(all);\n                }\n\n                ByteBuffer b = ByteBufferList.EMPTY_BYTEBUFFER;\n                while (true) {\n                    if (b.remaining() == 0 && buffered.size() > 0) {\n                        b = buffered.remove();\n                    }\n                    int remaining = b.remaining();\n                    int before = pending.remaining();\n\n                    SSLEngineResult res;\n                    {\n                        // wrap to prevent access to the readBuf\n                        ByteBuffer readBuf = allocator.allocate();\n                        res = engine.unwrap(b, readBuf);\n                        addToPending(pending, readBuf);\n                        allocator.track(pending.remaining() - before);\n                    }\n                    if (res.getStatus() == Status.BUFFER_OVERFLOW) {\n                        allocator.setMinAlloc(allocator.getMinAlloc() * 2);\n                        remaining = -1;\n                    }\n                    else if (res.getStatus() == Status.BUFFER_UNDERFLOW) {\n                        buffered.addFirst(b);\n                        if (buffered.size() <= 1) {\n                            break;\n                        }\n                        // pack it\n                        remaining = -1;\n                        b = buffered.getAll();\n                        buffered.addFirst(b);\n                        b = ByteBufferList.EMPTY_BYTEBUFFER;\n                    }\n                    handleHandshakeStatus(res.getHandshakeStatus());\n                    if (b.remaining() == remaining && before == pending.remaining()) {\n                        buffered.addFirst(b);\n                        break;\n                    }\n                }\n\n                AsyncSSLSocketWrapper.this.onDataAvailable();\n            }\n            catch (SSLException ex) {\n//                ex.printStackTrace();\n                report(ex);\n            }\n            finally {\n                mUnwrapping = false;\n            }\n        }\n    };\n\n    public void onDataAvailable() {\n        Util.emitAllData(this, pending);\n\n        if (mEnded && !pending.hasRemaining() && mEndCallback != null)\n            mEndCallback.onCompleted(mEndException);\n    }\n\n\n    @Override\n    public SSLEngine getSSLEngine() {\n        return engine;\n    }\n\n    void addToPending(ByteBufferList out, ByteBuffer mReadTmp) {\n        mReadTmp.flip();\n        if (mReadTmp.hasRemaining()) {\n            out.add(mReadTmp);\n        }\n        else {\n            ByteBufferList.reclaim(mReadTmp);\n        }\n    }\n\n\n    @Override\n    public void end() {\n        mSocket.end();\n    }\n\n    public String getHost() {\n        return mHost;\n    }\n\n    public int getPort() {\n        return mPort;\n    }\n\n    private void handleHandshakeStatus(HandshakeStatus status) {\n        if (status == HandshakeStatus.NEED_TASK) {\n            final Runnable task = engine.getDelegatedTask();\n            task.run();\n        }\n\n        if (status == HandshakeStatus.NEED_WRAP) {\n            write(writeList);\n        }\n\n        if (status == HandshakeStatus.NEED_UNWRAP) {\n            dataCallback.onDataAvailable(this, new ByteBufferList());\n        }\n\n        try {\n            if (!finishedHandshake && (engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING || engine.getHandshakeStatus() == HandshakeStatus.FINISHED)) {\n                if (clientMode) {\n                    Exception peerUnverifiedCause = null;\n                    boolean trusted = false;\n                    try {\n                        peerCertificates = (X509Certificate[]) engine.getSession().getPeerCertificates();\n                        if (mHost != null) {\n                            if (hostnameVerifier == null) {\n                                StrictHostnameVerifier verifier = new StrictHostnameVerifier();\n                                verifier.verify(mHost, StrictHostnameVerifier.getCNs(peerCertificates[0]), StrictHostnameVerifier.getDNSSubjectAlts(peerCertificates[0]));\n                            }\n                            else {\n                                if (!hostnameVerifier.verify(mHost, engine.getSession())) {\n                                    throw new SSLException(\"hostname <\" + mHost + \"> has been denied\");\n                                }\n                            }\n                        }\n\n                        trusted = true;\n                    }\n                    catch (SSLException ex) {\n                        peerUnverifiedCause = ex;\n                    }\n\n                    finishedHandshake = true;\n                    if (!trusted) {\n                        AsyncSSLException e = new AsyncSSLException(peerUnverifiedCause);\n                        report(e);\n                        if (!e.getIgnore())\n                            throw e;\n                    }\n                }\n                else {\n                    finishedHandshake = true;\n                }\n                handshakeCallback.onHandshakeCompleted(null, this);\n                handshakeCallback = null;\n\n                mSocket.setClosedCallback(null);\n                // handshake can complete during a wrap, so make sure that the call\n                // stack and wrap flag is cleared before invoking writable\n                getServer().post(new Runnable() {\n                    @Override\n                    public void run() {\n                        if (mWriteableCallback != null)\n                            mWriteableCallback.onWriteable();\n                    }\n                });\n                onDataAvailable();\n            }\n        }\n        catch (Exception ex) {\n            report(ex);\n        }\n    }\n\n    int calculateAlloc(int remaining) {\n        // alloc 50% more than we need for writing\n        int alloc = remaining * 3 / 2;\n        if (alloc == 0)\n            alloc = 8192;\n        return alloc;\n    }\n\n    ByteBufferList writeList = new ByteBufferList();\n    @Override\n    public void write(ByteBufferList bb) {\n        if (mWrapping)\n            return;\n        if (mSink.remaining() > 0)\n            return;\n        mWrapping = true;\n        int remaining;\n        SSLEngineResult res = null;\n        ByteBuffer writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining()));\n        do {\n            // if the handshake is finished, don't send\n            // 0 bytes of data, since that makes the ssl connection die.\n            // it wraps a 0 byte package, and craps out.\n            if (finishedHandshake && bb.remaining() == 0)\n                break;\n            remaining = bb.remaining();\n            try {\n                ByteBuffer[] arr = bb.getAllArray();\n                res = engine.wrap(arr, writeBuf);\n                bb.addAll(arr);\n                writeBuf.flip();\n                writeList.add(writeBuf);\n                assert !writeList.hasRemaining();\n                if (writeList.remaining() > 0)\n                    mSink.write(writeList);\n                int previousCapacity = writeBuf.capacity();\n                writeBuf = null;\n                if (res.getStatus() == Status.BUFFER_OVERFLOW) {\n                    writeBuf = ByteBufferList.obtain(previousCapacity * 2);\n                    remaining = -1;\n                }\n                else {\n                    writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining()));\n                    handleHandshakeStatus(res.getHandshakeStatus());\n                }\n            }\n            catch (SSLException e) {\n                report(e);\n            }\n        }\n        while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0);\n        mWrapping = false;\n        ByteBufferList.reclaim(writeBuf);\n    }\n\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        mWriteableCallback = handler;\n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        return mWriteableCallback;\n    }\n\n    private void report(Exception e) {\n        final HandshakeCallback hs = handshakeCallback;\n        if (hs != null) {\n            handshakeCallback = null;\n            mSocket.setDataCallback(new DataCallback.NullDataCallback());\n            mSocket.end();\n            // handshake sets this callback. unset it.\n            mSocket.setClosedCallback(null);\n            mSocket.close();\n            hs.onHandshakeCompleted(e, null);\n            return;\n        }\n\n        CompletedCallback cb = getEndCallback();\n        if (cb != null)\n            cb.onCompleted(e);\n    }\n\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        mDataCallback = callback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return mDataCallback;\n    }\n\n    @Override\n    public boolean isChunked() {\n        return mSocket.isChunked();\n    }\n\n    @Override\n    public boolean isOpen() {\n        return mSocket.isOpen();\n    }\n\n    @Override\n    public void close() {\n        mSocket.close();\n    }\n\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        mSocket.setClosedCallback(handler);\n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        return mSocket.getClosedCallback();\n    }\n\n    CompletedCallback mEndCallback;\n    @Override\n    public void setEndCallback(CompletedCallback callback) {\n        mEndCallback = callback;\n    }\n\n    @Override\n    public CompletedCallback getEndCallback() {\n        return mEndCallback;\n    }\n\n    @Override\n    public void pause() {\n        mSocket.pause();\n    }\n\n    @Override\n    public void resume() {\n        mSocket.resume();\n        onDataAvailable();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return mSocket.isPaused();\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mSocket.getServer();\n    }\n\n    @Override\n    public AsyncSocket getSocket() {\n        return mSocket;\n    }\n\n    @Override\n    public DataEmitter getDataEmitter() {\n        return mSocket;\n    }\n\n    @Override\n    public X509Certificate[] getPeerCertificates() {\n        return peerCertificates;\n    }\n\n    @Override\n    public String charset() {\n        return null;\n    }\n\n    private static Certificate selfSign(KeyPair keyPair, String subjectDN) throws Exception\n    {\n        Provider bcProvider = new BouncyCastleProvider();\n        Security.addProvider(bcProvider);\n\n        long now = System.currentTimeMillis();\n        Date startDate = new Date(now);\n\n        X500Name dnName = new X500Name(\"CN=\" + subjectDN);\n        BigInteger certSerialNumber = new BigInteger(Long.toString(now)); // <-- Using the current timestamp as the certificate serial number\n\n        Calendar calendar = Calendar.getInstance();\n        calendar.setTime(startDate);\n        calendar.add(Calendar.YEAR, 1); // <-- 1 Yr validity\n\n        Date endDate = calendar.getTime();\n\n        String signatureAlgorithm = \"SHA256WithRSA\"; // <-- Use appropriate signature algorithm based on your keyPair algorithm.\n\n        ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate());\n\n        JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName, keyPair.getPublic());\n\n        // Extensions --------------------------\n\n        // Basic Constraints\n        BasicConstraints basicConstraints = new BasicConstraints(true); // <-- true for CA, false for EndEntity\n\n        certBuilder.addExtension(new ASN1ObjectIdentifier(\"2.5.29.19\"), true, basicConstraints); // Basic Constraints is usually marked as critical.\n\n        // -------------------------------------\n\n        return new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(contentSigner));\n    }\n\n    public static Pair<KeyPair, Certificate> selfSignCertificate(final Context context, String subjectName) throws Exception {\n        File keyPath = context.getFileStreamPath(subjectName + \"-key.txt\");\n        KeyPair pair;\n        Certificate cert;\n        try {\n            String[] keyParts = StreamUtility.readFile(keyPath).split(\"\\n\");\n            X509EncodedKeySpec pub = new X509EncodedKeySpec(Base64.decode(keyParts[0], 0));\n            PKCS8EncodedKeySpec priv = new PKCS8EncodedKeySpec(Base64.decode(keyParts[1], 0));\n\n            cert = CertificateFactory.getInstance(\"X.509\").generateCertificate(new ByteArrayInputStream(Base64.decode(keyParts[2], 0)));\n\n            KeyFactory fact = KeyFactory.getInstance(\"RSA\");\n\n            pair = new KeyPair(fact.generatePublic(pub), fact.generatePrivate(priv));\n\n        }\n        catch (Exception e) {\n            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(\"RSA\");\n            keyGen.initialize(2048);\n            pair = keyGen.generateKeyPair();\n\n            cert = selfSign(pair, subjectName);\n\n            StreamUtility.writeFile(keyPath,\n                    Base64.encodeToString(pair.getPublic().getEncoded(), Base64.NO_WRAP)\n                            + \"\\n\"\n                            + Base64.encodeToString(pair.getPrivate().getEncoded(), Base64.NO_WRAP)\n                            + \"\\n\"\n                            + Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP));\n        }\n\n        return new Pair<>(pair, cert);\n    }\n\n    public static AsyncSSLServerSocket listenSecure(final Context context, final AsyncServer server, final String subjectName, final InetAddress host, final int port, final ListenCallback handler) {\n        final ObjectHolder<AsyncSSLServerSocket> holder = new ObjectHolder<>();\n        server.run(() -> {\n            try {\n                Pair<KeyPair, Certificate> keyCert = selfSignCertificate(context, subjectName);\n                KeyPair pair = keyCert.first;\n                Certificate cert = keyCert.second;\n\n                holder.held = listenSecure(server, pair.getPrivate(), cert, host, port, handler);\n            }\n            catch (Exception e) {\n                handler.onCompleted(e);\n            }\n        });\n        return holder.held;\n    }\n\n    public static AsyncSSLServerSocket listenSecure(AsyncServer server, String keyDer, String certDer, final InetAddress host, final int port, final ListenCallback handler) {\n        return listenSecure(server, Base64.decode(keyDer, Base64.DEFAULT), Base64.decode(certDer, Base64.DEFAULT), host, port, handler);\n    }\n\n    private static class ObjectHolder<T> {\n        T held;\n    }\n\n    public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final byte[] keyDer, final byte[] certDer, final InetAddress host, final int port, final ListenCallback handler) {\n        final ObjectHolder<AsyncSSLServerSocket> holder = new ObjectHolder<>();\n        server.run(() -> {\n            try {\n                PKCS8EncodedKeySpec key = new PKCS8EncodedKeySpec(keyDer);\n                Certificate cert = CertificateFactory.getInstance(\"X.509\").generateCertificate(new ByteArrayInputStream(certDer));\n\n                PrivateKey pk = KeyFactory.getInstance(\"RSA\").generatePrivate(key);\n\n                holder.held = listenSecure(server, pk, cert, host, port, handler);\n            }\n            catch (Exception e) {\n                handler.onCompleted(e);\n            }\n        });\n        return holder.held;\n    }\n\n    public static AsyncSSLServerSocket listenSecure(final AsyncServer server, final PrivateKey pk, final Certificate cert, final InetAddress host, final int port, final ListenCallback handler) {\n        final ObjectHolder<AsyncSSLServerSocket> holder = new ObjectHolder<>();\n        server.run(() -> {\n            try {\n                KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());\n                ks.load(null);\n\n                ks.setKeyEntry(\"key\", pk, null, new Certificate[] { cert });\n\n                KeyManagerFactory kmf = KeyManagerFactory.getInstance(\"X509\");\n                kmf.init(ks, \"\".toCharArray());\n\n                TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\n                tmf.init(ks);\n\n                SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n                sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);\n\n                final AsyncServerSocket socket = listenSecure(server, sslContext, host, port, handler);\n                holder.held = new AsyncSSLServerSocket() {\n                    @Override\n                    public PrivateKey getPrivateKey() {\n                        return pk;\n                    }\n\n                    @Override\n                    public Certificate getCertificate() {\n                        return cert;\n                    }\n\n                    @Override\n                    public void stop() {\n                        socket.stop();\n                    }\n\n                    @Override\n                    public int getLocalPort() {\n                        return socket.getLocalPort();\n                    }\n                };\n            }\n            catch (Exception e) {\n                handler.onCompleted(e);\n            }\n        });\n        return holder.held;\n    }\n\n    public static AsyncServerSocket listenSecure(AsyncServer server, final SSLContext sslContext, final InetAddress host, final int port, final ListenCallback handler) {\n        final SSLEngineSNIConfigurator conf = new SSLEngineSNIConfigurator() {\n            @Override\n            public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) {\n                SSLEngine engine = super.createEngine(sslContext, peerHost, peerPort);\n//                String[] ciphers = engine.getEnabledCipherSuites();\n//                for (String cipher: ciphers) {\n//                    Log.i(LOGTAG, cipher);\n//                }\n\n                // todo: what's this for? some vestigal vysor code i think. required by audio mirroring?\n                engine.setEnabledCipherSuites(new String[] { \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\" });\n                return engine;\n            }\n        };\n        return server.listen(host, port, new ListenCallback() {\n            @Override\n            public void onAccepted(final AsyncSocket socket) {\n                AsyncSSLSocketWrapper.handshake(socket, null, port, conf.createEngine(sslContext, null, port), null, null, false,\n                        (e, sslSocket) -> {\n                            if (e != null) {\n                                // chrome seems to do some sort of SSL probe and cancels handshakes. not sure why.\n                                // i suspect it is to pick an optimal strong cipher.\n                                // seeing a lot of the following in the log (but no actual connection errors)\n                                // javax.net.ssl.SSLHandshakeException: error:10000416:SSL routines:OPENSSL_internal:SSLV3_ALERT_CERTIFICATE_UNKNOWN\n                                // seen on Shield TV running API 26\n                                // todo fix: conscrypt ssl context?\n//                                Log.e(LOGTAG, \"Error while handshaking\", e);\n                                socket.close();\n                                return;\n                            }\n                            handler.onAccepted(sslSocket);\n                        });\n            }\n\n            @Override\n            public void onListening(AsyncServerSocket socket) {\n                handler.onListening(socket);\n            }\n\n            @Override\n            public void onCompleted(Exception ex) {\n                handler.onCompleted(ex);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSemaphore.java",
    "content": "package com.jeffmony.async;\n\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.TimeUnit;\n\npublic class AsyncSemaphore {\n\n    Semaphore semaphore = new Semaphore(0);\n\n    public void acquire() throws InterruptedException {\n        ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread());\n        AsyncSemaphore last = threadQueue.waiter;\n        threadQueue.waiter = this;\n        Semaphore queueSemaphore = threadQueue.queueSemaphore;\n        try {\n            if (semaphore.tryAcquire())\n                return;\n\n            while (true) {\n                // run the queue\n                while (true) {\n                    Runnable run = threadQueue.remove();\n                    if (run == null)\n                        break;\n//                        Log.i(LOGTAG, \"Pumping for AsyncSemaphore\");\n                    run.run();\n                }\n\n                int permits = Math.max(1, queueSemaphore.availablePermits());\n                queueSemaphore.acquire(permits);\n                if (semaphore.tryAcquire())\n                    break;\n            }\n        }\n        finally {\n            threadQueue.waiter = last;\n        }\n    }\n\n    public boolean tryAcquire(long timeout, TimeUnit timeunit) throws InterruptedException {\n        long timeoutMs = TimeUnit.MILLISECONDS.convert(timeout, timeunit);\n        ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(Thread.currentThread());\n        AsyncSemaphore last = threadQueue.waiter;\n        threadQueue.waiter = this;\n        Semaphore queueSemaphore = threadQueue.queueSemaphore;\n\n        try {\n            if (semaphore.tryAcquire())\n                return true;\n\n            long start = System.currentTimeMillis();\n            do {\n                // run the queue\n                while (true) {\n                    Runnable run = threadQueue.remove();\n                    if (run == null)\n                        break;\n//                        Log.i(LOGTAG, \"Pumping for AsyncSemaphore\");\n                    run.run();\n                }\n\n                int permits = Math.max(1, queueSemaphore.availablePermits());\n                if (!queueSemaphore.tryAcquire(permits, timeoutMs, TimeUnit.MILLISECONDS))\n                    return false;\n                if (semaphore.tryAcquire())\n                    return true;\n            }\n            while (System.currentTimeMillis() - start < timeoutMs);\n            return false;\n        }\n        finally {\n            threadQueue.waiter = last;\n        }\n    }\n\n    public void release() {\n        semaphore.release();\n        ThreadQueue.release(this);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncServer.java",
    "content": "package com.jeffmony.async;\n\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.util.Log;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\nimport com.jeffmony.async.callback.ListenCallback;\nimport com.jeffmony.async.callback.SocketCreateCallback;\nimport com.jeffmony.async.callback.ValueFunction;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.future.SimpleCancellable;\nimport com.jeffmony.async.future.SimpleFuture;\nimport com.jeffmony.async.util.StreamUtility;\n\nimport java.io.IOException;\nimport java.net.Inet4Address;\nimport java.net.Inet6Address;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.SocketAddress;\nimport java.nio.channels.CancelledKeyException;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.ClosedSelectorException;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\nimport java.nio.channels.spi.SelectorProvider;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.PriorityQueue;\nimport java.util.Set;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class AsyncServer {\n    public static final String LOGTAG = \"NIO\";\n\n    private static class RunnableWrapper implements Runnable {\n        boolean hasRun;\n        Runnable runnable;\n        ThreadQueue threadQueue;\n        Handler handler;\n        @Override\n        public void run() {\n            synchronized (this) {\n                if (hasRun)\n                    return;\n                hasRun = true;\n            }\n            try {\n                runnable.run();\n            }\n            finally {\n                threadQueue.remove(this);\n                handler.removeCallbacks(this);\n\n                threadQueue = null;\n                handler = null;\n                runnable = null;\n            }\n        }\n    }\n\n    public static void post(Handler handler, Runnable runnable) {\n        RunnableWrapper wrapper = new RunnableWrapper();\n        ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(handler.getLooper().getThread());\n        wrapper.threadQueue = threadQueue;\n        wrapper.handler = handler;\n        wrapper.runnable = runnable;\n\n        // run it in a blocking AsyncSemaphore or a Handler, whichever gets to it first.\n        threadQueue.add(wrapper);\n        handler.post(wrapper);\n\n        // run the queue if the thread is blocking\n        threadQueue.queueSemaphore.release();\n    }\n\n    static {\n        try {\n            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO) {\n                java.lang.System.setProperty(\"java.net.preferIPv4Stack\", \"true\");\n                java.lang.System.setProperty(\"java.net.preferIPv6Addresses\", \"false\");\n            }\n        }\n        catch (Throwable ex) {\n        }\n    }\n\n    static AsyncServer mInstance = new AsyncServer();\n    public static AsyncServer getDefault() {\n        return mInstance;\n    }\n\n    private SelectorWrapper mSelector;\n\n    public boolean isRunning() {\n        return mSelector != null;\n    }\n\n    String mName;\n    public AsyncServer() {\n        this(null);\n    }\n\n    public AsyncServer(String name) {\n        if (name == null)\n            name = \"AsyncServer\";\n        mName = name;\n    }\n\n    private static ExecutorService synchronousWorkers = newSynchronousWorkers(\"AsyncServer-worker-\");\n    private static void wakeup(final SelectorWrapper selector) {\n        synchronousWorkers.execute(() -> {\n            try {\n                selector.wakeupOnce();\n            }\n            catch (Exception e) {\n            }\n        });\n    }\n\n    boolean killed;\n    public void kill() {\n        synchronized (this) {\n            killed = true;\n        }\n        stop(false);\n    }\n\n    int postCounter = 0;\n    public Cancellable postDelayed(Runnable runnable, long delay) {\n        Scheduled s;\n        synchronized (this) {\n            if (killed)\n                return SimpleCancellable.CANCELLED;\n\n            // Calculate when to run this queue item:\n            // If there is a delay (non-zero), add it to the current time\n            // When delay is zero, ensure that this follows all other\n            // zero-delay queue items. This is done by setting the\n            // \"time\" to the queue size. This will make sure it is before\n            // all time-delayed queue items (for all real world scenarios)\n            // as it will always be less than the current time and also remain\n            // behind all other immediately run queue items.\n            long time;\n            if (delay > 0)\n                time = SystemClock.elapsedRealtime() + delay;\n            else if (delay == 0)\n                time = postCounter++;\n            else if (mQueue.size() > 0)\n                time = Math.min(0, mQueue.peek().time - 1);\n            else\n                time = 0;\n            mQueue.add(s = new Scheduled(this, runnable, time));\n            // start the server up if necessary\n            if (mSelector == null)\n                run();\n            if (!isAffinityThread()) {\n                wakeup(mSelector);\n            }\n        }\n        return s;\n    }\n\n    public Cancellable postImmediate(Runnable runnable) {\n        if (Thread.currentThread() == getAffinity()) {\n            runnable.run();\n            return null;\n        }\n        return postDelayed(runnable, -1);\n    }\n\n    public Cancellable post(Runnable runnable) {\n        return postDelayed(runnable, 0);\n    }\n\n    public Cancellable post(final CompletedCallback callback, final Exception e) {\n        return post(() -> callback.onCompleted(e));\n    }\n\n    public void run(final Runnable runnable) {\n        if (Thread.currentThread() == mAffinity) {\n            post(runnable);\n            lockAndRunQueue(this, mQueue);\n            return;\n        }\n\n        final Semaphore semaphore;\n        synchronized (this) {\n            if (killed)\n                return;\n            semaphore = new Semaphore(0);\n            post(() -> {\n                runnable.run();\n                semaphore.release();\n            });\n        }\n        try {\n            semaphore.acquire();\n        }\n        catch (InterruptedException e) {\n            Log.e(LOGTAG, \"run\", e);\n        }\n    }\n\n    private static class Scheduled implements Cancellable, Runnable {\n        // this constructor is only called when the async execution should not be preserved\n        // ie... AsyncServer.stop.\n        public Scheduled(AsyncServer server, Runnable runnable, long time) {\n            this.server = server;\n            this.runnable = runnable;\n            this.time = time;\n        }\n        public AsyncServer server;\n        public Runnable runnable;\n        public long time;\n\n        @Override\n        public void run() {\n            this.runnable.run();\n        }\n\n        @Override\n        public boolean isDone() {\n            synchronized (server) {\n                return !cancelled && !server.mQueue.contains(this);\n            }\n        }\n\n        boolean cancelled;\n        @Override\n        public boolean isCancelled() {\n            return cancelled;\n        }\n\n        @Override\n        public boolean cancel() {\n            synchronized (server) {\n                return cancelled = server.mQueue.remove(this);\n            }\n        }\n    }\n    PriorityQueue<Scheduled> mQueue = new PriorityQueue<Scheduled>(1, Scheduler.INSTANCE);\n\n    static class Scheduler implements Comparator<Scheduled> {\n        public static Scheduler INSTANCE = new Scheduler();\n        private Scheduler() {\n        }\n        @Override\n        public int compare(Scheduled s1, Scheduled s2) {\n            // keep the smaller ones at the head, so they get tossed out quicker\n            if (s1.time == s2.time)\n                return 0;\n            if (s1.time > s2.time)\n                return 1;\n            return -1;\n        }\n    }\n\n\n    public void stop() {\n        stop(false);\n    }\n\n    public void stop(boolean wait) {\n//        Log.i(LOGTAG, \"****AsyncServer is shutting down.****\");\n        final SelectorWrapper currentSelector;\n        final Semaphore semaphore;\n        final boolean isAffinityThread;\n        synchronized (this) {\n            isAffinityThread = isAffinityThread();\n            currentSelector = mSelector;\n            if (currentSelector == null)\n                return;\n            semaphore = new Semaphore(0);\n\n            // post a shutdown and wait\n            mQueue.add(new Scheduled(this, new Runnable() {\n                @Override\n                public void run() {\n                    shutdownEverything(currentSelector);\n                    semaphore.release();\n                }\n            }, 0));\n            synchronousWorkers.execute(() -> {\n                try {\n                    currentSelector.wakeupOnce();\n                }\n                catch (Exception e) {\n                }\n            });\n\n            // force any existing connections to die\n            shutdownKeys(currentSelector);\n\n            mQueue = new PriorityQueue<>(1, Scheduler.INSTANCE);\n            mSelector = null;\n            mAffinity = null;\n        }\n        try {\n            if (!isAffinityThread && wait)\n                semaphore.acquire();\n        }\n        catch (Exception e) {\n        }\n    }\n\n    protected void onDataReceived(int transmitted) {\n    }\n\n    protected void onDataSent(int transmitted) {\n    }\n\n    private static class ObjectHolder<T> {\n        T held;\n    }\n    public AsyncServerSocket listen(final InetAddress host, final int port, final ListenCallback handler) {\n        final ObjectHolder<AsyncServerSocket> holder = new ObjectHolder<>();\n        run(new Runnable() {\n            @Override\n            public void run() {\n                ServerSocketChannel closeableServer = null;\n                ServerSocketChannelWrapper closeableWrapper = null;\n                try {\n                    closeableServer = ServerSocketChannel.open();\n                    closeableWrapper = new ServerSocketChannelWrapper(\n                            closeableServer);\n                    final ServerSocketChannel server = closeableServer;\n                    final ServerSocketChannelWrapper wrapper = closeableWrapper;\n                    InetSocketAddress isa;\n                    if (host == null)\n                        isa = new InetSocketAddress(port);\n                    else\n                        isa = new InetSocketAddress(host, port);\n                    server.socket().bind(isa);\n                    final SelectionKey key = wrapper.register(mSelector.getSelector());\n                    key.attach(handler);\n                    handler.onListening(holder.held = new AsyncServerSocket() {\n                        @Override\n                        public int getLocalPort() {\n                            return server.socket().getLocalPort();\n                        }\n\n                        @Override\n                        public void stop() {\n                            StreamUtility.closeQuietly(wrapper);\n                            try {\n                                key.cancel();\n                            }\n                            catch (Exception e) {\n                            }\n                        }\n                    });\n                }\n                catch (IOException e) {\n                    Log.e(LOGTAG, \"wtf\", e);\n                    StreamUtility.closeQuietly(closeableWrapper, closeableServer);\n                    handler.onCompleted(e);\n                }\n            }\n        });\n        return holder.held;\n    }\n\n    private class ConnectFuture extends SimpleFuture<AsyncNetworkSocket> {\n        @Override\n        protected void cancelCleanup() {\n            super.cancelCleanup();\n            try {\n                if (socket != null)\n                    socket.close();\n            }\n            catch (IOException e) {\n            }\n        }\n\n        SocketChannel socket;\n        ConnectCallback callback;\n    }\n\n    public Cancellable connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback) {\n        return connectResolvedInetSocketAddress(address, callback, null);\n    }\n\n    public ConnectFuture connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback, final SocketCreateCallback createCallback) {\n        final ConnectFuture cancel = new ConnectFuture();\n        assert !address.isUnresolved();\n\n        post(new Runnable() {\n            @Override\n            public void run() {\n                if (cancel.isCancelled())\n                    return;\n\n                cancel.callback = callback;\n                SelectionKey ckey = null;\n                SocketChannel socket = null;\n                try {\n                    socket = cancel.socket = SocketChannel.open();\n                    socket.configureBlocking(false);\n                    ckey = socket.register(mSelector.getSelector(), SelectionKey.OP_CONNECT);\n                    ckey.attach(cancel);\n                    if (createCallback != null)\n                        createCallback.onSocketCreated(socket.socket().getLocalPort());\n                    socket.connect(address);\n                }\n                catch (Throwable e) {\n                    if (ckey != null)\n                        ckey.cancel();\n                    StreamUtility.closeQuietly(socket);\n                    cancel.setComplete(new RuntimeException(e));\n                }\n            }\n        });\n\n        return cancel;\n    }\n\n    public Cancellable connectSocket(final InetSocketAddress remote, final ConnectCallback callback) {\n        if (!remote.isUnresolved())\n            return connectResolvedInetSocketAddress(remote, callback);\n\n        final SimpleFuture<AsyncNetworkSocket> ret = new SimpleFuture<AsyncNetworkSocket>();\n\n        Future<InetAddress> lookup = getByName(remote.getHostName());\n        ret.setParent(lookup);\n        lookup\n        .setCallback(new FutureCallback<InetAddress>() {\n            @Override\n            public void onCompleted(Exception e, InetAddress result) {\n                if (e != null) {\n                    callback.onConnectCompleted(e, null);\n                    ret.setComplete(e);\n                    return;\n                }\n\n                ret.setComplete((ConnectFuture)connectResolvedInetSocketAddress(new InetSocketAddress(result, remote.getPort()), callback));\n            }\n        });\n        return ret;\n    }\n\n    public Cancellable connectSocket(final String host, final int port, final ConnectCallback callback) {\n        return connectSocket(InetSocketAddress.createUnresolved(host, port), callback);\n    }\n\n    private static ExecutorService newSynchronousWorkers(String prefix) {\n        ThreadFactory tf = new NamedThreadFactory(prefix);\n        ThreadPoolExecutor tpe = new ThreadPoolExecutor(1, 4, 10L,\n            TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), tf);\n        return tpe;\n    }\n\n    private static final Comparator<InetAddress> ipSorter = new Comparator<InetAddress>() {\n        @Override\n        public int compare(InetAddress lhs, InetAddress rhs) {\n            if (lhs instanceof Inet4Address && rhs instanceof Inet4Address)\n                return 0;\n            if (lhs instanceof Inet6Address && rhs instanceof Inet6Address)\n                return 0;\n            if (lhs instanceof Inet4Address && rhs instanceof Inet6Address)\n                return -1;\n            return 1;\n        }\n    };\n\n    private static ExecutorService synchronousResolverWorkers = newSynchronousWorkers(\"AsyncServer-resolver-\");\n    public Future<InetAddress[]> getAllByName(final String host) {\n        final SimpleFuture<InetAddress[]> ret = new SimpleFuture<InetAddress[]>();\n        synchronousResolverWorkers.execute(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    final InetAddress[] result = InetAddress.getAllByName(host);\n                    Arrays.sort(result, ipSorter);\n                    if (result == null || result.length == 0)\n                        throw new HostnameResolutionException(\"no addresses for host\");\n                    post(new Runnable() {\n                        @Override\n                        public void run() {\n                            ret.setComplete(null, result);\n                        }\n                    });\n                } catch (final Exception e) {\n                    post(new Runnable() {\n                        @Override\n                        public void run() {\n                            ret.setComplete(e, null);\n                        }\n                    });\n                }\n            }\n        });\n        return ret;\n    }\n\n    public Future<InetAddress> getByName(String host) {\n        return getAllByName(host).thenConvert(addresses -> addresses[0]);\n    }\n\n    private void handleSocket(final AsyncNetworkSocket handler) throws ClosedChannelException {\n        final ChannelWrapper sc = handler.getChannel();\n        SelectionKey ckey = sc.register(mSelector.getSelector());\n        ckey.attach(handler);\n        handler.setup(this, ckey);\n    }\n\n    public AsyncDatagramSocket connectDatagram(final String host, final int port) throws IOException {\n        final DatagramChannel socket = DatagramChannel.open();\n        final AsyncDatagramSocket handler = new AsyncDatagramSocket();\n        handler.attach(socket);\n        // ugh.. this should really be post to make it nonblocking...\n        // but i want datagrams to be immediately writable.\n        // they're not really used anyways.\n        run(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    final SocketAddress remote = new InetSocketAddress(host, port);\n                    handleSocket(handler);\n                    socket.connect(remote);\n                }\n                catch (IOException e) {\n                    Log.e(LOGTAG, \"Datagram error\", e);\n                    StreamUtility.closeQuietly(socket);\n                }\n            }\n        });\n        return handler;\n    }\n\n    public AsyncDatagramSocket openDatagram() {\n        return openDatagram(null, 0, false);\n    }\n\n    public Cancellable createDatagram(String address, int port, boolean reuseAddress, FutureCallback<AsyncDatagramSocket> callback) {\n        return createDatagram(() -> InetAddress.getByName(address), port, reuseAddress, callback);\n    }\n\n    public Cancellable createDatagram(InetAddress address, int port, boolean reuseAddress, FutureCallback<AsyncDatagramSocket> callback) {\n        return createDatagram(() -> address, port, reuseAddress, callback);\n    }\n\n    private Cancellable createDatagram(ValueFunction<InetAddress> inetAddressValueFunction, final int port, final boolean reuseAddress, FutureCallback<AsyncDatagramSocket> callback) {\n        SimpleFuture<AsyncDatagramSocket> ret = new SimpleFuture<>();\n        ret.setCallback(callback);\n        post(() -> {\n            DatagramChannel socket = null;\n            try {\n                socket = DatagramChannel.open();\n\n                final AsyncDatagramSocket handler = new AsyncDatagramSocket();\n                handler.attach(socket);\n\n                InetSocketAddress address;\n                if (inetAddressValueFunction == null)\n                    address = new InetSocketAddress(port);\n                else\n                    address = new InetSocketAddress(inetAddressValueFunction.getValue(), port);\n\n                if (reuseAddress)\n                    socket.socket().setReuseAddress(reuseAddress);\n                socket.socket().bind(address);\n                handleSocket(handler);\n                if (!ret.setComplete(handler))\n                    socket.close();\n            }\n            catch (Exception e) {\n                StreamUtility.closeQuietly(socket);\n                ret.setComplete(e);\n            }\n        });\n\n        return ret;\n    }\n\n    public AsyncDatagramSocket openDatagram(final InetAddress host, final int port, final boolean reuseAddress) {\n        final AsyncDatagramSocket handler = new AsyncDatagramSocket();\n        // ugh.. this should really be post to make it nonblocking...\n        // but i want datagrams to be immediately writable.\n        // they're not really used anyways.\n        Runnable runnable = () -> {\n            final DatagramChannel socket;\n            try {\n                socket = DatagramChannel.open();\n            }\n            catch (Exception e) {\n                return;\n            }\n            try {\n                handler.attach(socket);\n\n                InetSocketAddress address;\n                if (host == null)\n                    address = new InetSocketAddress(port);\n                else\n                    address = new InetSocketAddress(host, port);\n\n                if (reuseAddress)\n                    socket.socket().setReuseAddress(reuseAddress);\n                socket.socket().bind(address);\n                handleSocket(handler);\n            }\n            catch (IOException e) {\n                Log.e(LOGTAG, \"Datagram error\", e);\n                StreamUtility.closeQuietly(socket);\n            }\n        };\n\n        if (getAffinity() != Thread.currentThread()) {\n            run(runnable);\n            return handler;\n        }\n\n        runnable.run();\n        return handler;\n    }\n\n    public AsyncDatagramSocket connectDatagram(final SocketAddress remote) throws IOException {\n        final AsyncDatagramSocket handler = new AsyncDatagramSocket();\n        final DatagramChannel socket = DatagramChannel.open();\n        handler.attach(socket);\n        // ugh.. this should really be post to make it nonblocking...\n        // but i want datagrams to be immediately writable.\n        // they're not really used anyways.\n        Runnable runnable = () -> {\n            try {\n                handleSocket(handler);\n                socket.connect(remote);\n            }\n            catch (IOException e) {\n                StreamUtility.closeQuietly(socket);\n            }\n        };\n\n        if (getAffinity() != Thread.currentThread()) {\n            run(runnable);\n            return handler;\n        }\n\n        runnable.run();\n        return handler;\n    }\n\n    final private static ThreadLocal<AsyncServer> threadServer = new ThreadLocal<>();\n\n    public static AsyncServer getCurrentThreadServer() {\n        return threadServer.get();\n    }\n\n    Thread mAffinity;\n    private void run() {\n        final SelectorWrapper selector;\n        final PriorityQueue<Scheduled> queue;\n        synchronized (this) {\n            if (mSelector == null) {\n                try {\n                    selector = mSelector = new SelectorWrapper(SelectorProvider.provider().openSelector());\n                    queue = mQueue;\n                }\n                catch (IOException e) {\n                    throw new RuntimeException(\"unable to create selector?\", e);\n                }\n\n                mAffinity = new Thread(mName) {\n                    public void run() {\n                        try {\n                            threadServer.set(AsyncServer.this);\n                            AsyncServer.run(AsyncServer.this, selector, queue);\n                        }\n                        finally {\n                            threadServer.remove();\n                        }\n                    }\n                };\n\n                mAffinity.start();\n                // kicked off the new thread, let's bail.\n                return;\n            }\n\n            // this is a reentrant call\n            selector = mSelector;\n            queue = mQueue;\n\n            // fall through to outside of the synchronization scope\n            // to allow the thread to run without locking.\n        }\n\n        try {\n            runLoop(this, selector, queue);\n        }\n        catch (AsyncSelectorException e) {\n            Log.i(LOGTAG, \"Selector closed\", e);\n            try {\n                // StreamUtility.closeQuiety is throwing ArrayStoreException?\n                selector.getSelector().close();\n            }\n            catch (Exception ex) {\n            }\n        }\n    }\n\n    private static void run(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue<Scheduled> queue) {\n//        Log.i(LOGTAG, \"****AsyncServer is starting.****\");\n        // at this point, this local queue and selector are owned\n        // by this thread.\n        // if a stop is called, the instance queue and selector\n        // will be replaced and nulled respectively.\n        // this will allow the old queue and selector to shut down\n        // gracefully, while also allowing a new selector thread\n        // to start up while the old one is still shutting down.\n        while(true) {\n            try {\n                runLoop(server, selector, queue);\n            }\n            catch (AsyncSelectorException e) {\n                if (!(e.getCause() instanceof ClosedSelectorException))\n                    Log.i(LOGTAG, \"Selector exception, shutting down\", e);\n                StreamUtility.closeQuietly(selector);\n            }\n            // see if we keep looping, this must be in a synchronized block since the queue is accessed.\n            synchronized (server) {\n                if (selector.isOpen() && (selector.keys().size() > 0 || queue.size() > 0))\n                    continue;\n\n                shutdownEverything(selector);\n                if (server.mSelector == selector) {\n                    server.mQueue = new PriorityQueue<Scheduled>(1, Scheduler.INSTANCE);\n                    server.mSelector = null;\n                    server.mAffinity = null;\n                }\n                break;\n            }\n        }\n//        Log.i(LOGTAG, \"****AsyncServer has shut down.****\");\n    }\n\n    private static void shutdownKeys(SelectorWrapper selector) {\n        try {\n            for (SelectionKey key: selector.keys()) {\n                StreamUtility.closeQuietly(key.channel());\n                try {\n                    key.cancel();\n                }\n                catch (Exception e) {\n                }\n            }\n        }\n        catch (Exception ex) {\n        }\n    }\n\n    private static void shutdownEverything(SelectorWrapper selector) {\n        shutdownKeys(selector);\n        // SHUT. DOWN. EVERYTHING.\n        StreamUtility.closeQuietly(selector);\n    }\n\n    private static final long QUEUE_EMPTY = Long.MAX_VALUE;\n    private static long lockAndRunQueue(final AsyncServer server, final PriorityQueue<Scheduled> queue) {\n        long wait = QUEUE_EMPTY;\n\n        // find the first item we can actually run\n        while (true) {\n            Scheduled run = null;\n\n            synchronized (server) {\n                long now = SystemClock.elapsedRealtime();\n\n                if (queue.size() > 0) {\n                    Scheduled s = queue.remove();\n                    if (s.time <= now) {\n                        run = s;\n                    }\n                    else {\n                        wait = s.time - now;\n                        queue.add(s);\n                    }\n                }\n            }\n\n            if (run == null)\n                break;\n\n            run.run();\n        }\n\n        server.postCounter = 0;\n        return wait;\n    }\n\n    private static class AsyncSelectorException extends IOException {\n        public AsyncSelectorException(Exception e) {\n            super(e);\n        }\n    }\n\n    private static void runLoop(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue<Scheduled> queue) throws AsyncSelectorException {\n//        Log.i(LOGTAG, \"Keys: \" + selector.keys().size());\n        boolean needsSelect = true;\n\n        // run the queue to populate the selector with keys\n        long wait = lockAndRunQueue(server, queue);\n        try {\n            synchronized (server) {\n                // select now to see if anything is ready immediately. this\n                // also clears the canceled key queue.\n                int readyNow = selector.selectNow();\n                if (readyNow == 0) {\n                    // if there is nothing to select now, make sure we don't have an empty key set\n                    // which means it would be time to turn this thread off.\n                    if (selector.keys().size() == 0 && wait == QUEUE_EMPTY) {\n//                    Log.i(LOGTAG, \"Shutting down. keys: \" + selector.keys().size() + \" keepRunning: \" + keepRunning);\n                        return;\n                    }\n                }\n                else {\n                    needsSelect = false;\n                }\n            }\n\n            if (needsSelect) {\n                if (wait == QUEUE_EMPTY) {\n                    // wait until woken up\n                    selector.select();\n                }\n                else {\n                    // nothing to select immediately but there's something pending so let's block that duration and wait.\n                    selector.select(wait);\n                }\n            }\n        }\n        catch (Exception e) {\n            throw new AsyncSelectorException(e);\n        }\n\n        // process whatever keys are ready\n        Set<SelectionKey> readyKeys = selector.selectedKeys();\n        for (SelectionKey key: readyKeys) {\n            try {\n                if (key.isAcceptable()) {\n                    ServerSocketChannel nextReady = (ServerSocketChannel) key.channel();\n                    SocketChannel sc = null;\n                    SelectionKey ckey = null;\n                    try {\n                        sc = nextReady.accept();\n                        if (sc == null)\n                            continue;\n                        sc.configureBlocking(false);\n                        ckey = sc.register(selector.getSelector(), SelectionKey.OP_READ);\n                        ListenCallback serverHandler = (ListenCallback) key.attachment();\n                        AsyncNetworkSocket handler = new AsyncNetworkSocket();\n                        handler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress());\n                        handler.setup(server, ckey);\n                        ckey.attach(handler);\n                        serverHandler.onAccepted(handler);\n                    }\n                    catch (IOException e) {\n                        StreamUtility.closeQuietly(sc);\n                        if (ckey != null)\n                            ckey.cancel();\n                    }\n                }\n                else if (key.isReadable()) {\n                    AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment();\n                    int transmitted = handler.onReadable();\n                    server.onDataReceived(transmitted);\n                }\n                else if (key.isWritable()) {\n                    AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment();\n                    handler.onDataWritable();\n                }\n                else if (key.isConnectable()) {\n                    ConnectFuture cancel = (ConnectFuture) key.attachment();\n                    SocketChannel sc = (SocketChannel) key.channel();\n                    key.interestOps(SelectionKey.OP_READ);\n                    AsyncNetworkSocket newHandler;\n                    try {\n                        sc.finishConnect();\n                        newHandler = new AsyncNetworkSocket();\n                        newHandler.setup(server, key);\n                        newHandler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress());\n                        key.attach(newHandler);\n                    }\n                    catch (IOException ex) {\n                        key.cancel();\n                        StreamUtility.closeQuietly(sc);\n                        if (cancel.setComplete(ex))\n                            cancel.callback.onConnectCompleted(ex, null);\n                        continue;\n                    }\n                    if (cancel.setComplete(newHandler))\n                        cancel.callback.onConnectCompleted(null, newHandler);\n                }\n                else {\n                    Log.i(LOGTAG, \"wtf\");\n                    throw new RuntimeException(\"Unknown key state.\");\n                }\n            }\n            catch (CancelledKeyException ex) {\n            }\n        }\n        readyKeys.clear();\n    }\n\n    public void dump() {\n        post(new Runnable() {\n            @Override\n            public void run() {\n                if (mSelector == null) {\n                    Log.i(LOGTAG, \"Server dump not possible. No selector?\");\n                    return;\n                }\n                Log.i(LOGTAG, \"Key Count: \" + mSelector.keys().size());\n\n                for (SelectionKey key: mSelector.keys()) {\n                    Log.i(LOGTAG, \"Key: \" + key);\n                }\n            }\n        });\n    }\n\n    public Thread getAffinity() {\n        return mAffinity;\n    }\n\n    public boolean isAffinityThread() {\n        return mAffinity == Thread.currentThread();\n    }\n\n    public boolean isAffinityThreadOrStopped() {\n        Thread affinity = mAffinity;\n        return affinity == null || affinity == Thread.currentThread();\n    }\n\n    private static class NamedThreadFactory implements ThreadFactory {\n        private final ThreadGroup group;\n        private final AtomicInteger threadNumber = new AtomicInteger(1);\n        private final String namePrefix;\n\n        NamedThreadFactory(String namePrefix) {\n            SecurityManager s = System.getSecurityManager();\n            group = (s != null) ? s.getThreadGroup() :\n                Thread.currentThread().getThreadGroup();\n            this.namePrefix = namePrefix;\n        }\n\n        public Thread newThread(Runnable r) {\n            Thread t = new Thread(group, r,\n                namePrefix + threadNumber.getAndIncrement(), 0);\n            if (t.isDaemon()) t.setDaemon(false);\n            if (t.getPriority() != Thread.NORM_PRIORITY) {\n                t.setPriority(Thread.NORM_PRIORITY);\n            }\n            return t;\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncServerSocket.java",
    "content": "package com.jeffmony.async;\n\npublic interface AsyncServerSocket {\n    void stop();\n    int getLocalPort();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/AsyncSocket.java",
    "content": "package com.jeffmony.async;\n\npublic interface AsyncSocket extends DataEmitter, DataSink {\n    AsyncServer getServer();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/BufferedDataSink.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.WritableCallback;\n\npublic class BufferedDataSink implements DataSink {\n    DataSink mDataSink;\n    public BufferedDataSink(DataSink datasink) {\n        setDataSink(datasink);\n    }\n\n    public boolean isBuffering() {\n        return mPendingWrites.hasRemaining() || forceBuffering;\n    }\n\n    public boolean isWritable() {\n        synchronized (mPendingWrites) {\n            return mPendingWrites.remaining() < mMaxBuffer;\n        }\n    }\n\n    public DataSink getDataSink() {\n        return mDataSink;\n    }\n\n    boolean forceBuffering;\n    public void forceBuffering(boolean forceBuffering) {\n        this.forceBuffering = forceBuffering;\n        if (!forceBuffering)\n            writePending();\n    }\n\n    public void setDataSink(DataSink datasink) {\n        mDataSink = datasink;\n        mDataSink.setWriteableCallback(this::writePending);\n    }\n\n    private void writePending() {\n        if (forceBuffering)\n            return;\n\n//        Log.i(\"NIO\", \"Writing to buffer...\");\n        boolean empty;\n        synchronized (mPendingWrites) {\n            mDataSink.write(mPendingWrites);\n            empty = mPendingWrites.isEmpty();\n        }\n        if (empty) {\n            if (endPending)\n                mDataSink.end();\n        }\n        if (empty && mWritable != null)\n            mWritable.onWriteable();\n    }\n    \n    final ByteBufferList mPendingWrites = new ByteBufferList();\n\n    // before the data is queued, let inheritors know. allows for filters, without\n    // issues with having to filter before writing which may fail in the buffer.\n    protected void onDataAccepted(ByteBufferList bb) {\n    }\n\n    @Override\n    public void write(final ByteBufferList bb) {\n        if (getServer().getAffinity() != Thread.currentThread()) {\n            synchronized (mPendingWrites) {\n                if (mPendingWrites.remaining() >= mMaxBuffer)\n                    return;\n                onDataAccepted(bb);\n                bb.get(mPendingWrites);\n            }\n            getServer().post(this::writePending);\n            return;\n        }\n\n        onDataAccepted(bb);\n\n        if (!isBuffering())\n            mDataSink.write(bb);\n\n        synchronized (mPendingWrites) {\n            bb.get(mPendingWrites);\n        }\n    }\n\n    WritableCallback mWritable;\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        mWritable = handler;\n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        return mWritable;\n    }\n    \n    public int remaining() {\n        return mPendingWrites.remaining();\n    }\n    \n    int mMaxBuffer = Integer.MAX_VALUE;\n    public int getMaxBuffer() {\n        return mMaxBuffer;\n    }\n\n    public void setMaxBuffer(int maxBuffer) {\n        assert maxBuffer >= 0;\n        mMaxBuffer = maxBuffer;\n    }\n\n    @Override\n    public boolean isOpen() {\n        return mDataSink.isOpen();\n    }\n\n    boolean endPending;\n    @Override\n    public void end() {\n        if (getServer().getAffinity() != Thread.currentThread()) {\n            getServer().post(this::end);\n            return;\n        }\n\n        synchronized (mPendingWrites) {\n            if (mPendingWrites.hasRemaining()) {\n                endPending = true;\n                return;\n            }\n        }\n        mDataSink.end();\n    }\n\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        mDataSink.setClosedCallback(handler);\n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        return mDataSink.getClosedCallback();\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mDataSink.getServer();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/ByteBufferList.java",
    "content": "package com.jeffmony.async;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.os.Looper;\n\nimport com.jeffmony.async.util.ArrayDeque;\nimport com.jeffmony.async.util.Charsets;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.charset.Charset;\nimport java.util.Comparator;\nimport java.util.PriorityQueue;\n\n@TargetApi(Build.VERSION_CODES.GINGERBREAD)\npublic class ByteBufferList {\n    ArrayDeque<ByteBuffer> mBuffers = new ArrayDeque<ByteBuffer>();\n    \n    ByteOrder order = ByteOrder.BIG_ENDIAN;\n    public ByteOrder order() {\n        return order;\n    }\n\n    public ByteBufferList order(ByteOrder order) {\n        this.order = order;\n        return this;\n    }\n\n    public ByteBufferList() {\n    }\n\n    public ByteBufferList(ByteBuffer... b) {\n        addAll(b);\n    }\n\n    public ByteBufferList(byte[] buf) {\n        super();\n        ByteBuffer b = ByteBuffer.wrap(buf);\n        add(b);\n    }\n\n    public ByteBufferList addAll(ByteBuffer... bb) {\n        for (ByteBuffer b: bb)\n            add(b);\n        return this;\n    }\n\n    public ByteBufferList addAll(ByteBufferList... bb) {\n        for (ByteBufferList b: bb)\n            b.get(this);\n        return this;\n    }\n\n    public byte[] getBytes(int length) {\n        byte[] ret = new byte[length];\n        get(ret);\n        return ret;\n    }\n\n    public byte[] getAllByteArray() {\n        byte[] ret = new byte[remaining()];\n        get(ret);\n        return ret;\n    }\n\n    public ByteBuffer[] getAllArray() {\n        ByteBuffer[] ret = new ByteBuffer[mBuffers.size()];\n        ret = mBuffers.toArray(ret);\n        mBuffers.clear();\n        remaining = 0;\n        return ret;\n    }\n\n    public boolean isEmpty() {\n        return remaining == 0;\n    }\n\n    private int remaining = 0;\n    public int remaining() {\n        return remaining;\n    }\n\n    public boolean hasRemaining() {\n        return remaining() > 0;\n    }\n\n    public short peekShort() {\n        return read(2).getShort(mBuffers.peekFirst().position());\n    }\n\n    public byte peek() {\n        return read(1).get(mBuffers.peekFirst().position());\n    }\n\n    public int peekInt() {\n        return read(4).getInt(mBuffers.peekFirst().position());\n    }\n\n    public long peekLong() {\n        return read(8).getLong(mBuffers.peekFirst().position());\n    }\n\n    public byte[] peekBytes(int size) {\n        byte[] ret = new byte[size];\n        read(size).get(ret, mBuffers.peekFirst().position(), ret.length);\n        return ret;\n    }\n\n    public ByteBufferList skip(int length) {\n        get(null, 0, length);\n        return this;\n    }\n\n    public int getInt() {\n        int ret = read(4).getInt();\n        remaining -= 4;\n        return ret;\n    }\n    \n    public char getByteChar() {\n        char ret = (char)read(1).get();\n        remaining--;\n        return ret;\n    }\n    \n    public short getShort() {\n        short ret = read(2).getShort();\n        remaining -= 2;\n        return ret;\n    }\n    \n    public byte get() {\n        byte ret = read(1).get();\n        remaining--;\n        return ret;\n    }\n    \n    public long getLong() {\n        long ret = read(8).getLong();\n        remaining -= 8;\n        return ret;\n    }\n\n    public void get(byte[] bytes) {\n        get(bytes, 0, bytes.length);\n    }\n\n    public void get(byte[] bytes, int offset, int length) {\n        if (remaining() < length)\n            throw new IllegalArgumentException(\"length\");\n\n        int need = length;\n        while (need > 0) {\n            ByteBuffer b = mBuffers.peek();\n            int read = Math.min(b.remaining(), need);\n            if (bytes != null){\n                b.get(bytes, offset, read);\n            } else {\n                //when bytes is null, just skip data.\n                b.position(b.position() + read);\n            }\n            need -= read;\n            offset += read;\n            if (b.remaining() == 0) {\n                ByteBuffer removed = mBuffers.remove();\n                assert b == removed;\n                reclaim(b);\n            }\n        }\n\n        remaining -= length;\n    }\n\n    public void get(ByteBufferList into, int length) {\n        if (remaining() < length)\n            throw new IllegalArgumentException(\"length\");\n        int offset = 0;\n\n        while (offset < length) {\n            ByteBuffer b = mBuffers.remove();\n            int remaining = b.remaining();\n\n            if (remaining == 0) {\n                reclaim(b);\n                continue;\n            }\n\n            if (offset + remaining > length) {\n                int need = length - offset;\n                // this is shared between both\n                ByteBuffer subset = obtain(need);\n                subset.limit(need);\n                b.get(subset.array(), 0, need);\n                into.add(subset);\n                mBuffers.addFirst(b);\n                assert subset.capacity() >= need;\n                assert subset.position() == 0;\n                break;\n            }\n            else {\n                // this belongs to the new list\n                into.add(b);\n            }\n\n            offset += remaining;\n        }\n\n        remaining -= length;\n    }\n\n    public void get(ByteBufferList into) {\n        get(into, remaining());\n    }\n\n    public ByteBufferList get(int length) {\n        ByteBufferList ret = new ByteBufferList();\n        get(ret, length);\n        return ret.order(order);\n    }\n\n    public ByteBuffer getAll() {\n        if (remaining() == 0)\n            return EMPTY_BYTEBUFFER;\n        read(remaining());\n        return remove();\n    }\n\n    private ByteBuffer read(int count) {\n        if (remaining() < count)\n            throw new IllegalArgumentException(\"count : \" + remaining() + \"/\" + count);\n\n        ByteBuffer first = mBuffers.peek();\n        while (first != null && !first.hasRemaining()) {\n            reclaim(mBuffers.remove());\n            first = mBuffers.peek();\n        }\n        \n        if (first == null) {\n            return EMPTY_BYTEBUFFER;\n        }\n\n        if (first.remaining() >= count) {\n            return first.order(order);\n        }\n\n        ByteBuffer ret = obtain(count);\n        ret.limit(count);\n        byte[] bytes = ret.array();\n        int offset = 0;\n        ByteBuffer bb = null;\n        while (offset < count) {\n            bb = mBuffers.remove();\n            int toRead = Math.min(count - offset, bb.remaining());\n            bb.get(bytes, offset, toRead);\n            offset += toRead;\n            if (bb.remaining() == 0) {\n                reclaim(bb);\n                bb = null;\n            }\n        }\n        // if there was still data left in the last buffer we popped\n        // toss it back into the head\n        if (bb != null && bb.remaining() > 0)\n            mBuffers.addFirst(bb);\n        mBuffers.addFirst(ret);\n        return ret.order(order);\n    }\n    \n    public void trim() {\n        // this clears out buffers that are empty in the beginning of the list\n        read(0);\n    }\n\n    public ByteBufferList add(ByteBufferList b) {\n        b.get(this);\n        return this;\n    }\n\n    public ByteBufferList add(ByteBuffer b) {\n        if (b.remaining() <= 0) {\n//            System.out.println(\"reclaiming remaining: \" + b.remaining());\n            reclaim(b);\n            return this;\n        }\n        addRemaining(b.remaining());\n        // see if we can fit the entirety of the buffer into the end\n        // of the current last buffer\n        if (mBuffers.size() > 0) {\n            ByteBuffer last = mBuffers.getLast();\n            if (last.capacity() - last.limit() >= b.remaining()) {\n                last.mark();\n                last.position(last.limit());\n                last.limit(last.capacity());\n                last.put(b);\n                last.limit(last.position());\n                last.reset();\n                reclaim(b);\n                trim();\n                return this;\n            }\n        }\n        mBuffers.add(b);\n        trim();\n        return this;\n    }\n\n    public void addFirst(ByteBuffer b) {\n        if (b.remaining() <= 0) {\n            reclaim(b);\n            return;\n        }\n        addRemaining(b.remaining());\n        // see if we can fit the entirety of the buffer into the beginning\n        // of the current first buffer\n        if (mBuffers.size() > 0) {\n            ByteBuffer first = mBuffers.getFirst();\n            if (first.position() >= b.remaining()) {\n                first.position(first.position() - b.remaining());\n                first.mark();\n                first.put(b);\n                first.reset();\n                reclaim(b);\n                return;\n            }\n        }\n        mBuffers.addFirst(b);\n    }\n\n    private void addRemaining(int remaining) {\n        if (this.remaining() >= 0)\n            this.remaining += remaining;\n    }\n\n    public void recycle() {\n        while (mBuffers.size() > 0) {\n            reclaim(mBuffers.remove());\n        }\n        assert mBuffers.size() == 0;\n        remaining = 0;\n    }\n    \n    public ByteBuffer remove() {\n        ByteBuffer ret = mBuffers.remove();\n        remaining -= ret.remaining();\n        return ret;\n    }\n    \n    public int size() {\n        return mBuffers.size();\n    }\n\n    public void spewString() {\n        System.out.println(peekString());\n    }\n\n    public String peekString() {\n        return peekString(null);\n    }\n\n    // not doing toString as this is really nasty in the debugger...\n    public String peekString(Charset charset) {\n        if (charset == null)\n            charset = Charsets.UTF_8;\n        StringBuilder builder = new StringBuilder();\n        for (ByteBuffer bb: mBuffers) {\n            byte[] bytes;\n            int offset;\n            int length;\n            if (bb.isDirect()) {\n                bytes = new byte[bb.remaining()];\n                offset = 0;\n                length = bb.remaining();\n                bb.get(bytes);\n            }\n            else {\n                bytes = bb.array();\n                offset = bb.arrayOffset() + bb.position();\n                length = bb.remaining();\n            }\n            builder.append(new String(bytes, offset, length, charset));\n        }\n        return builder.toString();\n    }\n\n    public String readString() {\n        return readString(null);\n    }\n\n    public String readString(Charset charset) {\n        String ret = peekString(charset);\n        recycle();\n        return ret;\n    }\n\n    static class Reclaimer implements Comparator<ByteBuffer> {\n        @Override\n        public int compare(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) {\n            // keep the smaller ones at the head, so they get tossed out quicker\n            if (byteBuffer.capacity() == byteBuffer2.capacity())\n                return 0;\n            if (byteBuffer.capacity() > byteBuffer2.capacity())\n                return 1;\n            return -1;\n        }\n    }\n\n    static PriorityQueue<ByteBuffer> reclaimed = new PriorityQueue<ByteBuffer>(8, new Reclaimer());\n\n    private static PriorityQueue<ByteBuffer> getReclaimed() {\n        Looper mainLooper = Looper.getMainLooper();\n        if (mainLooper != null) {\n            if (Thread.currentThread() == mainLooper.getThread())\n                return null;\n        }\n        return reclaimed;\n    }\n\n    private static int MAX_SIZE = 1024 * 1024;\n    public static int MAX_ITEM_SIZE = 1024 * 256;\n    static int currentSize = 0;\n    static int maxItem = 0;\n\n    public static void setMaxPoolSize(int size) {\n        MAX_SIZE = size;\n    }\n\n    public static void setMaxItemSize(int size) {\n        MAX_ITEM_SIZE = size;\n    }\n\n    private static boolean reclaimedContains(ByteBuffer b) {\n        for (ByteBuffer other: reclaimed) {\n            if (other == b)\n                return true;\n        }\n        return false;\n    }\n\n    public static void reclaim(ByteBuffer b) {\n        if (b == null || b.isDirect())\n            return;\n        if (b.arrayOffset() != 0 || b.array().length != b.capacity())\n            return;\n        if (b.capacity() < 8192)\n            return;\n        if (b.capacity() > MAX_ITEM_SIZE)\n            return;\n\n        PriorityQueue<ByteBuffer> r = getReclaimed();\n        if (r == null)\n            return;\n\n        synchronized (LOCK) {\n            while (currentSize > MAX_SIZE && r.size() > 0 && r.peek().capacity() < b.capacity()) {\n//                System.out.println(\"removing for better: \" + b.capacity());\n                ByteBuffer head = r.remove();\n                currentSize -= head.capacity();\n            }\n\n            if (currentSize > MAX_SIZE) {\n//                System.out.println(\"too full: \" + b.capacity());\n                return;\n            }\n\n            assert !reclaimedContains(b);\n\n            b.position(0);\n            b.limit(b.capacity());\n            currentSize += b.capacity();\n\n            r.add(b);\n            assert r.size() != 0 ^ currentSize == 0;\n\n            maxItem = Math.max(maxItem, b.capacity());\n        }\n    }\n\n    private static final Object LOCK = new Object();\n\n    public static ByteBuffer obtain(int size) {\n        if (size <= maxItem) {\n            PriorityQueue<ByteBuffer> r = getReclaimed();\n            if (r != null) {\n                synchronized (LOCK) {\n                    while (r.size() > 0) {\n                        ByteBuffer ret = r.remove();\n                        if (r.size() == 0)\n                            maxItem = 0;\n                        currentSize -= ret.capacity();\n                        assert r.size() != 0 ^ currentSize == 0;\n                        if (ret.capacity() >= size) {\n//                            System.out.println(\"using \" + ret.capacity());\n                            return ret;\n                        }\n//                        System.out.println(\"dumping \" + ret.capacity());\n                    }\n                }\n            }\n        }\n\n//        System.out.println(\"alloc for \" + size);\n        ByteBuffer ret = ByteBuffer.allocate(Math.max(8192, size));\n        return ret;\n    }\n\n    public static void obtainArray(ByteBuffer[] arr, int size) {\n        PriorityQueue<ByteBuffer> r = getReclaimed();\n        int index = 0;\n        int total = 0;\n\n        if (r != null) {\n            synchronized (LOCK) {\n                while (r.size() > 0 && total < size && index < arr.length - 1) {\n                    ByteBuffer b = r.remove();\n                    currentSize -= b.capacity();\n                    assert r.size() != 0 ^ currentSize == 0;\n                    int needed = Math.min(size - total, b.capacity());\n                    total += needed;\n                    arr[index++] = b;\n                }\n            }\n        }\n\n        if (total < size) {\n            ByteBuffer b = ByteBuffer.allocate(Math.max(8192, size - total));\n            arr[index++] = b;\n        }\n\n        for (int i = index; i < arr.length; i++) {\n            arr[i] = EMPTY_BYTEBUFFER;\n        }\n    }\n\n    public static ByteBuffer deepCopy(ByteBuffer copyOf) {\n        if (copyOf == null)\n            return null;\n        return (ByteBuffer)obtain(copyOf.remaining()).put(copyOf.duplicate()).flip();\n    }\n\n    public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);\n\n    public static void writeOutputStream(OutputStream out, ByteBuffer b) throws IOException {\n        byte[] bytes;\n        int offset;\n        int length;\n        if (b.isDirect()) {\n            bytes = new byte[b.remaining()];\n            offset = 0;\n            length = b.remaining();\n            b.get(bytes);\n        }\n        else {\n            bytes = b.array();\n            offset = b.arrayOffset() + b.position();\n            length = b.remaining();\n        }\n        out.write(bytes, offset, length);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/ChannelWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.channels.ScatteringByteChannel;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.spi.AbstractSelectableChannel;\n\nabstract class ChannelWrapper implements ReadableByteChannel, ScatteringByteChannel {\n    private AbstractSelectableChannel mChannel;\n    ChannelWrapper(AbstractSelectableChannel channel) throws IOException {\n        channel.configureBlocking(false);\n        mChannel = channel;\n    }\n\n    public abstract void shutdownInput();\n    public abstract void shutdownOutput();\n    \n    public abstract boolean isConnected();\n    \n    public abstract int write(ByteBuffer src) throws IOException;\n    public abstract int write(ByteBuffer[] src) throws IOException;\n\n    // register for default events appropriate for this channel\n    public abstract SelectionKey register(Selector sel) throws ClosedChannelException;\n\n    public SelectionKey register(Selector sel, int ops) throws ClosedChannelException {\n        return mChannel.register(sel, ops);\n    }\n\n    public boolean isChunked() {\n        return false;\n    }\n    \n    @Override\n    public boolean isOpen() {\n        return mChannel.isOpen();\n    }\n    \n    @Override\n    public void close() throws IOException {\n       mChannel.close();\n    }\n    \n    public abstract int getLocalPort();\n    public abstract InetAddress getLocalAddress();\n    public abstract Object getSocket();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DataEmitter.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\n\npublic interface DataEmitter {\n    void setDataCallback(DataCallback callback);\n    DataCallback getDataCallback();\n    boolean isChunked();\n    void pause();\n    void resume();\n    void close();\n    boolean isPaused();\n    void setEndCallback(CompletedCallback callback);\n    CompletedCallback getEndCallback();\n    AsyncServer getServer();\n    String charset();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DataEmitterBase.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic abstract class DataEmitterBase implements DataEmitter {\n    private boolean ended;\n    protected void report(Exception e) {\n        if (ended)\n            return;\n        ended = true;\n        if (getEndCallback() != null)\n            getEndCallback().onCompleted(e);\n    }\n\n    @Override\n    public final void setEndCallback(CompletedCallback callback) {\n        endCallback = callback;\n    }\n\n    CompletedCallback endCallback;\n    @Override\n    public final CompletedCallback getEndCallback() {\n        return endCallback;\n    }\n\n\n    DataCallback mDataCallback;\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        mDataCallback = callback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return mDataCallback;\n    }\n\n    @Override\n    public String charset() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DataEmitterReader.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.DataCallback;\n\npublic class DataEmitterReader implements DataCallback {\n    DataCallback mPendingRead;\n    int mPendingReadLength;\n    ByteBufferList mPendingData = new ByteBufferList();\n\n    public void read(int count, DataCallback callback) {\n        assert mPendingRead == null;\n        mPendingReadLength = count;\n        mPendingRead = callback;\n        assert !mPendingData.hasRemaining();\n        mPendingData.recycle();\n    }\n\n    private boolean handlePendingData(DataEmitter emitter) {\n        if (mPendingReadLength > mPendingData.remaining())\n            return false;\n\n        DataCallback pendingRead = mPendingRead;\n        mPendingRead = null;\n        pendingRead.onDataAvailable(emitter, mPendingData);\n        assert !mPendingData.hasRemaining();\n\n        return true;\n    }\n\n    public DataEmitterReader() {\n    }\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        // if we're registered for data, we must be waiting for a read\n        assert mPendingRead != null;\n        do {\n            int need = Math.min(bb.remaining(), mPendingReadLength - mPendingData.remaining());\n            bb.get(mPendingData, need);\n            bb.remaining();\n        }\n        while (handlePendingData(emitter) && mPendingRead != null);\n        bb.remaining();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DataSink.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.WritableCallback;\n\npublic interface DataSink {\n    public void write(ByteBufferList bb);\n    public void setWriteableCallback(WritableCallback handler);\n    public WritableCallback getWriteableCallback();\n    \n    public boolean isOpen();\n    public void end();\n    public void setClosedCallback(CompletedCallback handler);\n    public CompletedCallback getClosedCallback();\n    public AsyncServer getServer();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DataTrackingEmitter.java",
    "content": "package com.jeffmony.async;\n\n/**\n * Created by koush on 5/28/13.\n */\npublic interface DataTrackingEmitter extends DataEmitter {\n    interface DataTracker {\n        void onData(int totalBytesRead);\n    }\n    void setDataTracker(DataTracker tracker);\n    DataTracker getDataTracker();\n    int getBytesRead();\n    void setDataEmitter(DataEmitter emitter);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/DatagramChannelWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.DatagramChannel;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\n\nclass DatagramChannelWrapper extends ChannelWrapper {\n    DatagramChannel mChannel;\n\n    @Override\n    public InetAddress getLocalAddress() {\n        return mChannel.socket().getLocalAddress();\n    }\n\n    @Override\n    public int getLocalPort() {\n        return mChannel.socket().getLocalPort();\n    }\n\n    InetSocketAddress address;\n    public InetSocketAddress getRemoteAddress() {\n        return address;\n    }\n    \n    public void disconnect() throws IOException {\n        mChannel.disconnect();\n    }\n    \n    DatagramChannelWrapper(DatagramChannel channel) throws IOException {\n        super(channel);\n        mChannel = channel;\n    }\n    @Override\n    public int read(ByteBuffer buffer) throws IOException {\n        if (!isConnected()) {\n            int position = buffer.position();\n            address = (InetSocketAddress)mChannel.receive(buffer);\n            if (address == null)\n                return -1;\n            return buffer.position() - position;\n        }\n        address = null;\n        return mChannel.read(buffer);\n    }\n    @Override\n    public boolean isConnected() {\n        return mChannel.isConnected();\n    }\n    @Override\n    public int write(ByteBuffer src) throws IOException {\n        return mChannel.write(src);\n    }\n    @Override\n    public int write(ByteBuffer[] src) throws IOException {\n        return (int)mChannel.write(src);\n    }\n    @Override\n    public SelectionKey register(Selector sel, int ops) throws ClosedChannelException {\n        return mChannel.register(sel, ops);\n    }\n    @Override\n    public boolean isChunked() {\n        return true;\n    }\n    @Override\n    public SelectionKey register(Selector sel) throws ClosedChannelException {\n        return register(sel, SelectionKey.OP_READ);\n    }\n\n    @Override\n    public void shutdownOutput() {\n    }\n\n    @Override\n    public void shutdownInput() {\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers) throws IOException {\n        return mChannel.read(byteBuffers);\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException {\n        return mChannel.read(byteBuffers, i, i2);\n    }\n\n    @Override\n    public Object getSocket() {\n        return mChannel.socket();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/FileDataEmitter.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.util.StreamUtility;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\n\n/**\n * Created by koush on 5/22/13.\n */\npublic class FileDataEmitter extends DataEmitterBase {\n    AsyncServer server;\n    File file;\n    public FileDataEmitter(AsyncServer server, File file) {\n        this.server = server;\n        this.file = file;\n        paused = !server.isAffinityThread();\n        if (!paused)\n            doResume();\n    }\n\n    DataCallback callback;\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        this.callback = callback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return callback;\n    }\n\n    @Override\n    public boolean isChunked() {\n        return false;\n    }\n\n    boolean paused;\n    @Override\n    public void pause() {\n        paused = true;\n    }\n\n    @Override\n    public void resume() {\n        paused = false;\n        doResume();\n    }\n\n    @Override\n    protected void report(Exception e) {\n        StreamUtility.closeQuietly(channel);\n        super.report(e);\n    }\n\n    ByteBufferList pending = new ByteBufferList();\n    FileChannel channel;\n    Runnable pumper = new Runnable() {\n        @Override\n        public void run() {\n            try {\n                if (channel == null)\n                    channel = new FileInputStream(file).getChannel();\n                if (!pending.isEmpty()) {\n                    Util.emitAllData(FileDataEmitter.this, pending);\n                    if (!pending.isEmpty())\n                        return;\n                }\n                ByteBuffer b;\n                do {\n                    b = ByteBufferList.obtain(8192);\n                    if (-1 == channel.read(b)) {\n                        report(null);\n                        return;\n                    }\n                    b.flip();\n                    pending.add(b);\n                    Util.emitAllData(FileDataEmitter.this, pending);\n                }\n                while (pending.remaining() == 0 && !isPaused());\n            }\n            catch (Exception e) {\n                report(e);\n            }\n        }\n    };\n\n    private void doResume() {\n        server.post(pumper);\n    }\n\n    @Override\n    public boolean isPaused() {\n        return paused;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return server;\n    }\n\n    @Override\n    public void close() {\n        try {\n            channel.close();\n        }\n        catch (Exception e) {\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/FilteredDataEmitter.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.wrapper.DataEmitterWrapper;\n\npublic class FilteredDataEmitter extends DataEmitterBase implements DataEmitter, DataCallback, DataEmitterWrapper, DataTrackingEmitter {\n    private DataEmitter mEmitter;\n    @Override\n    public DataEmitter getDataEmitter() {\n        return mEmitter;\n    }\n\n    @Override\n    public void setDataEmitter(DataEmitter emitter) {\n        if (mEmitter != null) {\n            mEmitter.setDataCallback(null);\n        }\n        mEmitter = emitter;\n        mEmitter.setDataCallback(this);\n        mEmitter.setEndCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                report(ex);\n            }\n        });\n    }\n\n    @Override\n    public int getBytesRead() {\n        return totalRead;\n    }\n\n    @Override\n    public DataTracker getDataTracker() {\n        return tracker;\n    }\n\n    @Override\n    public void setDataTracker(DataTracker tracker) {\n        this.tracker = tracker;\n    }\n\n    private DataTracker tracker;\n    private int totalRead;\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        if (closed) {\n            // this emitter was closed but for some reason data is still being spewed...\n            // eat it, nom nom.\n            bb.recycle();\n            return;\n        }\n        if (bb != null)\n            totalRead += bb.remaining();\n        Util.emitAllData(this, bb);\n        if (bb != null)\n            totalRead -= bb.remaining();\n        if (tracker != null && bb != null)\n            tracker.onData(totalRead);\n        // if there's data after the emitting, and it is paused... the underlying implementation\n        // is obligated to cache the byte buffer list.\n    }\n\n    @Override\n    public boolean isChunked() {\n        return mEmitter.isChunked();\n    }\n\n    @Override\n    public void pause() {\n        mEmitter.pause();\n    }\n\n    @Override\n    public void resume() {\n        mEmitter.resume();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return mEmitter.isPaused();\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mEmitter.getServer();\n    }\n\n    boolean closed;\n    @Override\n    public void close() {\n        closed = true;\n        if (mEmitter != null)\n            mEmitter.close();\n    }\n\n    @Override\n    public String charset() {\n        if (mEmitter == null)\n            return null;\n        return mEmitter.charset();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/FilteredDataSink.java",
    "content": "package com.jeffmony.async;\n\npublic class FilteredDataSink extends BufferedDataSink {\n    public FilteredDataSink(DataSink sink) {\n        super(sink);\n        setMaxBuffer(0);\n    }\n    \n    public ByteBufferList filter(ByteBufferList bb) {\n        return bb;\n    }\n\n    @Override\n    protected void onDataAccepted(ByteBufferList bb) {\n        ByteBufferList filtered = filter(bb);\n        // filtering may return the same byte buffer, so watch for that.\n        if (filtered != bb) {\n            bb.recycle();\n            filtered.get(bb);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/HostnameResolutionException.java",
    "content": "package com.jeffmony.async;\n\npublic class HostnameResolutionException extends Exception {\n    public HostnameResolutionException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/LineEmitter.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\n\npublic class LineEmitter implements DataCallback {\n    public interface StringCallback {\n        void onStringAvailable(String s);\n    }\n\n    public LineEmitter() {\n        this(null);\n    }\n\n    public LineEmitter(Charset charset) {\n        this.charset = charset;\n    }\n\n    Charset charset;\n\n    ByteBufferList data = new ByteBufferList();\n\n    StringCallback mLineCallback;\n    public void setLineCallback(StringCallback callback) {\n        mLineCallback = callback;\n    }\n\n    public StringCallback getLineCallback() {\n        return mLineCallback;\n    }\n\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        ByteBuffer buffer = ByteBuffer.allocate(bb.remaining());\n        while (bb.remaining() > 0) {\n            byte b = bb.get();\n            if (b == '\\n') {\n                assert mLineCallback != null;\n                buffer.flip();\n                data.add(buffer);\n                mLineCallback.onStringAvailable(data.readString(charset));\n                data = new ByteBufferList();\n                return;\n            }\n            else {\n                buffer.put(b);\n            }\n        }\n        buffer.flip();\n        data.add(buffer);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/PushParser.java",
    "content": "package com.jeffmony.async;\n\nimport android.util.Log;\n\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.lang.reflect.Method;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayList;\nimport java.util.Hashtable;\nimport java.util.LinkedList;\n\npublic class PushParser implements DataCallback {\n\n    public interface ParseCallback<T> {\n        public void parsed(T data);\n    }\n\n    static abstract class Waiter {\n        int length;\n        public Waiter(int length) {\n            this.length = length;\n        }\n        /**\n         * Consumes received data, and/or returns next waiter to continue reading instead of this waiter.\n         * @param bb received data, bb.remaining >= length\n         * @return - a waiter that should continue reading right away, or null if this waiter is finished\n         */\n        public abstract Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb);\n    }\n\n    static class IntWaiter extends Waiter {\n        ParseCallback<Integer> callback;\n        public IntWaiter(ParseCallback<Integer> callback) {\n            super(4);\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            callback.parsed(bb.getInt());\n            return null;\n        }\n    }\n\n    static class ByteArrayWaiter extends Waiter {\n        ParseCallback<byte[]> callback;\n        public ByteArrayWaiter(int length, ParseCallback<byte[]> callback) {\n            super(length);\n            if (length <= 0)\n                throw new IllegalArgumentException(\"length should be > 0\");\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            byte[] bytes = new byte[length];\n            bb.get(bytes);\n            callback.parsed(bytes);\n            return null;\n        }\n    }\n\n    static class LenByteArrayWaiter extends Waiter {\n        private final ParseCallback<byte[]> callback;\n\n        public LenByteArrayWaiter(ParseCallback<byte[]> callback) {\n            super(4);\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            int length = bb.getInt();\n            if (length == 0) {\n                callback.parsed(new byte[0]);\n                return null;\n            }\n            return new ByteArrayWaiter(length, callback);\n        }\n    }\n\n\n    static class ByteBufferListWaiter extends Waiter {\n        ParseCallback<ByteBufferList> callback;\n        public ByteBufferListWaiter(int length, ParseCallback<ByteBufferList> callback) {\n            super(length);\n            if (length <= 0) throw new IllegalArgumentException(\"length should be > 0\");\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            callback.parsed(bb.get(length));\n            return null;\n        }\n    }\n\n    static class LenByteBufferListWaiter extends Waiter {\n        private final ParseCallback<ByteBufferList> callback;\n\n        public LenByteBufferListWaiter(ParseCallback<ByteBufferList> callback) {\n            super(4);\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            int length = bb.getInt();\n            return new ByteBufferListWaiter(length, callback);\n        }\n    }\n\n    static class UntilWaiter extends Waiter {\n\n        byte value;\n        DataCallback callback;\n        public UntilWaiter(byte value, DataCallback callback) {\n            super(1);\n            this.value = value;\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            boolean found = true;\n            ByteBufferList cb = new ByteBufferList();\n            while (bb.size() > 0) {\n                ByteBuffer b = bb.remove();\n                b.mark();\n                int index = 0;\n                while (b.remaining() > 0 && !(found = (b.get() == value))) {\n                    index++;\n                }\n                b.reset();\n                if (found) {\n                    bb.addFirst(b);\n                    bb.get(cb, index);\n                    // eat the one we're waiting on\n                    bb.get();\n                    break;\n                } else {\n                    cb.add(b);\n                }\n            }\n\n            callback.onDataAvailable(emitter, cb);\n\n            if (found) {\n                return null;\n            } else {\n                return this;\n            }\n        }\n    }\n\n    private class TapWaiter extends Waiter {\n        private final TapCallback callback;\n\n        public TapWaiter(TapCallback callback) {\n            super(0);\n            this.callback = callback;\n        }\n\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            Method method = getTap(callback);\n            method.setAccessible(true);\n            try {\n                method.invoke(callback, args.toArray());\n            } catch (Exception e) {\n                Log.e(\"PushParser\", \"Error while invoking tap callback\", e);\n            }\n            args.clear();\n            return null;\n        }\n    }\n\n    private Waiter noopArgWaiter = new Waiter(0) {\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            args.add(null);\n            return null;\n        }\n    };\n\n    private Waiter byteArgWaiter = new Waiter(1) {\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            args.add(bb.get());\n            return null;\n        }\n    };\n\n    private Waiter shortArgWaiter = new Waiter(2) {\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            args.add(bb.getShort());\n            return null;\n        }\n    };\n\n    private Waiter intArgWaiter = new Waiter(4) {\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            args.add(bb.getInt());\n            return null;\n        }\n    };\n\n    private Waiter longArgWaiter = new Waiter(8) {\n        @Override\n        public Waiter onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            args.add(bb.getLong());\n            return null;\n        }\n    };\n\n    private ParseCallback<byte[]> byteArrayArgCallback = new ParseCallback<byte[]>() {\n        @Override\n        public void parsed(byte[] data) {\n            args.add(data);\n        }\n    };\n\n    private ParseCallback<ByteBufferList> byteBufferListArgCallback = new ParseCallback<ByteBufferList>() {\n        @Override\n        public void parsed(ByteBufferList data) {\n            args.add(data);\n        }\n    };\n\n    private ParseCallback<byte[]> stringArgCallback = new ParseCallback<byte[]>() {\n        @Override\n        public void parsed(byte[] data) {\n            args.add(new String(data));\n        }\n    };\n\n    DataEmitter mEmitter;\n    private LinkedList<Waiter> mWaiting = new LinkedList<Waiter>();\n    private ArrayList<Object> args = new ArrayList<Object>();\n    ByteOrder order = ByteOrder.BIG_ENDIAN;\n\n    public PushParser setOrder(ByteOrder order) {\n        this.order = order;\n        return this;\n    }\n\n    public PushParser(DataEmitter s) {\n        mEmitter = s;\n        mEmitter.setDataCallback(this);\n    }\n\n    public PushParser readInt(ParseCallback<Integer> callback) {\n        mWaiting.add(new IntWaiter(callback));\n        return this;\n    }\n\n    public PushParser readByteArray(int length, ParseCallback<byte[]> callback) {\n        mWaiting.add(new ByteArrayWaiter(length, callback));\n        return this;\n    }\n\n    public PushParser readByteBufferList(int length, ParseCallback<ByteBufferList> callback) {\n        mWaiting.add(new ByteBufferListWaiter(length, callback));\n        return this;\n    }\n\n    public PushParser until(byte b, DataCallback callback) {\n        mWaiting.add(new UntilWaiter(b, callback));\n        return this;\n    }\n\n    public PushParser readByte() {\n        mWaiting.add(byteArgWaiter);\n        return this;\n    }\n\n    public PushParser readShort() {\n        mWaiting.add(shortArgWaiter);\n        return this;\n    }\n\n    public PushParser readInt() {\n        mWaiting.add(intArgWaiter);\n        return this;\n    }\n\n    public PushParser readLong() {\n        mWaiting.add(longArgWaiter);\n        return this;\n    }\n\n    public PushParser readByteArray(int length) {\n        return (length == -1) ? readLenByteArray() : readByteArray(length, byteArrayArgCallback);\n    }\n\n    public PushParser readLenByteArray() {\n        mWaiting.add(new LenByteArrayWaiter(byteArrayArgCallback));\n        return this;\n    }\n\n    public PushParser readByteBufferList(int length) {\n        return (length == -1) ? readLenByteBufferList() : readByteBufferList(length, byteBufferListArgCallback);\n    }\n\n    public PushParser readLenByteBufferList() {\n        return readLenByteBufferList(byteBufferListArgCallback);\n    }\n\n    public PushParser readLenByteBufferList(ParseCallback<ByteBufferList> callback) {\n        mWaiting.add(new LenByteBufferListWaiter(callback));\n        return this;\n    }\n\n    public PushParser readString() {\n        mWaiting.add(new LenByteArrayWaiter(stringArgCallback));\n        return this;\n    }\n\n    public PushParser noop() {\n        mWaiting.add(noopArgWaiter);\n        return this;\n    }\n\n    ByteBufferList pending = new ByteBufferList();\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        bb.get(pending);\n        while (mWaiting.size() > 0 && pending.remaining() >= mWaiting.peek().length) {\n            pending.order(order);\n            Waiter next = mWaiting.poll().onDataAvailable(emitter, pending);\n            if (next != null) mWaiting.addFirst(next);\n        }\n        if (mWaiting.size() == 0)\n            pending.get(bb);\n    }\n\n    public void tap(TapCallback callback) {\n        mWaiting.add(new TapWaiter(callback));\n    }\n\n    static Hashtable<Class, Method> mTable = new Hashtable<Class, Method>();\n    static Method getTap(TapCallback callback) {\n        Method found = mTable.get(callback.getClass());\n        if (found != null)\n            return found;\n\n        for (Method method : callback.getClass().getMethods()) {\n            if (\"tap\".equals(method.getName())) {\n                mTable.put(callback.getClass(), method);\n                return method;\n            }\n        }\n\n        // try the proguard friendly route, take the first/only method\n        // in case \"tap\" has been renamed\n        Method[] candidates = callback.getClass().getDeclaredMethods();\n        if (candidates.length == 1)\n            return candidates[0];\n\n        String fail =\n            \"-keep class * extends com.koushikdutta.async.TapCallback {\\n\" +\n                    \"    *;\\n\" +\n                    \"}\\n\";\n\n        //null != \"AndroidAsync: tap callback could not be found. Proguard? Use this in your proguard config:\\n\" + fail;\n        throw new AssertionError(fail);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/SelectorWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.util.Set;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * Created by koush on 2/13/14.\n */\nclass SelectorWrapper implements Closeable {\n    private Selector selector;\n    public AtomicBoolean isWaking = new AtomicBoolean(false);\n    Semaphore semaphore = new Semaphore(0);\n    public Selector getSelector() {\n        return selector;\n    }\n\n    public SelectorWrapper(Selector selector) {\n        this.selector = selector;\n    }\n\n    public int selectNow() throws IOException {\n        return selector.selectNow();\n    }\n\n    public void select() throws IOException {\n        select(0);\n    }\n\n    public void select(long timeout) throws IOException {\n        try {\n            semaphore.drainPermits();\n            selector.select(timeout);\n        }\n        finally {\n            semaphore.release(Integer.MAX_VALUE);\n        }\n    }\n\n    public Set<SelectionKey> keys() {\n        return selector.keys();\n    }\n\n    public Set<SelectionKey> selectedKeys() {\n        return selector.selectedKeys();\n    }\n\n    @Override\n    public void close() throws IOException {\n        selector.close();\n    }\n\n    public boolean isOpen() {\n        return selector.isOpen();\n    }\n\n    public void wakeupOnce() {\n        // see if it is selecting, ie, can't acquire a permit\n        boolean selecting = !semaphore.tryAcquire();\n        selector.wakeup();\n        // if it was selecting, then the wakeup definitely worked.\n        if (selecting)\n            return;\n\n        // now, we NEED to wait for the select to start to forcibly wake it.\n        if (isWaking.getAndSet(true)) {\n            selector.wakeup();\n            return;\n        }\n\n        try {\n            waitForSelect();\n            selector.wakeup();\n        } finally {\n            isWaking.set(false);\n        }\n    }\n\n    public boolean waitForSelect() {\n        // try to wake up 10 times\n        for (int i = 0; i < 100; i++) {\n            try {\n                if (semaphore.tryAcquire(10, TimeUnit.MILLISECONDS)) {\n                    // successfully acquiring means the selector is NOT selecting, since select\n                    // will drain all permits.\n                    continue;\n                }\n            } catch (InterruptedException e) {\n                // an InterruptedException means the acquire failed a select is in progress,\n                // since it holds all permits\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/ServerSocketChannelWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.ServerSocketChannel;\n\nclass ServerSocketChannelWrapper extends ChannelWrapper {\n    ServerSocketChannel mChannel;\n\n    @Override\n    public void shutdownOutput() {\n    }\n\n    @Override\n    public void shutdownInput() {\n    }\n\n    @Override\n    public InetAddress getLocalAddress() {\n        return mChannel.socket().getInetAddress();\n    }\n\n    @Override\n    public int getLocalPort() {\n        return mChannel.socket().getLocalPort();\n    }\n\n    ServerSocketChannelWrapper(ServerSocketChannel channel) throws IOException {\n        super(channel);\n        mChannel = channel;\n    }\n\n    @Override\n    public int read(ByteBuffer buffer) throws IOException {\n        final String msg = \"Can't read ServerSocketChannel\";\n        assert false;\n        throw new IOException(msg);\n    }\n\n    @Override\n    public boolean isConnected() {\n        assert false;\n        return false;\n    }\n\n    @Override\n    public int write(ByteBuffer src) throws IOException {\n        final String msg = \"Can't write ServerSocketChannel\";\n        assert false;\n        throw new IOException(msg);\n    }\n\n    @Override\n    public SelectionKey register(Selector sel) throws ClosedChannelException {\n        return mChannel.register(sel, SelectionKey.OP_ACCEPT);\n    }\n\n    @Override\n    public int write(ByteBuffer[] src) throws IOException {\n        final String msg = \"Can't write ServerSocketChannel\";\n        assert false;\n        throw new IOException(msg);\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers) throws IOException {\n        final String msg = \"Can't read ServerSocketChannel\";\n        assert false;\n        throw new IOException(msg);\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException {\n        final String msg = \"Can't read ServerSocketChannel\";\n        assert false;\n        throw new IOException(msg);\n    }\n\n    @Override\n    public Object getSocket() {\n        return mChannel.socket();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/SocketChannelWrapper.java",
    "content": "package com.jeffmony.async;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedChannelException;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.SocketChannel;\n\nclass SocketChannelWrapper extends ChannelWrapper {\n    SocketChannel mChannel;\n\n    @Override\n    public InetAddress getLocalAddress() {\n        return mChannel.socket().getLocalAddress();\n    }\n\n    @Override\n    public int getLocalPort() {\n        return mChannel.socket().getLocalPort();\n    }\n\n    SocketChannelWrapper(SocketChannel channel) throws IOException {\n        super(channel);\n        mChannel = channel;\n    }\n    @Override\n    public int read(ByteBuffer buffer) throws IOException {\n        return mChannel.read(buffer);\n    }\n    @Override\n    public boolean isConnected() {\n        return mChannel.isConnected();\n    }\n    @Override\n    public int write(ByteBuffer src) throws IOException {\n        return mChannel.write(src);\n    }\n    @Override\n    public int write(ByteBuffer[] src) throws IOException {\n        return (int)mChannel.write(src);\n    }\n    @Override\n    public SelectionKey register(Selector sel) throws ClosedChannelException {\n        return register(sel, SelectionKey.OP_CONNECT);\n    }\n\n    @Override\n    public void shutdownOutput() {\n        try {\n            mChannel.socket().shutdownOutput();\n        }\n        catch (Exception e) {\n        }\n    }\n\n    @Override\n    public void shutdownInput() {\n        try {\n            mChannel.socket().shutdownInput();\n        }\n        catch (Exception e) {\n        }\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers) throws IOException {\n        return mChannel.read(byteBuffers);\n    }\n\n    @Override\n    public long read(ByteBuffer[] byteBuffers, int i, int i2) throws IOException {\n        return mChannel.read(byteBuffers, i, i2);\n    }\n\n    @Override\n    public Object getSocket() {\n        return mChannel.socket();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/TapCallback.java",
    "content": "package com.jeffmony.async;\n\n\npublic interface TapCallback {\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/ThreadQueue.java",
    "content": "package com.jeffmony.async;\n\nimport java.util.LinkedList;\nimport java.util.WeakHashMap;\nimport java.util.concurrent.Semaphore;\n\nclass ThreadQueue extends LinkedList<Runnable> {\n    final private static WeakHashMap<Thread, ThreadQueue> mThreadQueues = new WeakHashMap<Thread, ThreadQueue>();\n\n    static ThreadQueue getOrCreateThreadQueue(Thread thread) {\n        ThreadQueue queue;\n        synchronized (mThreadQueues) {\n            queue = mThreadQueues.get(thread);\n            if (queue == null) {\n                queue = new ThreadQueue();\n                mThreadQueues.put(thread, queue);\n            }\n        }\n\n        return queue;\n    }\n\n    static void release(AsyncSemaphore semaphore) {\n        synchronized (mThreadQueues) {\n            for (ThreadQueue threadQueue: mThreadQueues.values()) {\n                if (threadQueue.waiter == semaphore)\n                    threadQueue.queueSemaphore.release();\n            }\n        }\n    }\n\n    AsyncSemaphore waiter;\n    Semaphore queueSemaphore = new Semaphore(0);\n\n    @Override\n    public boolean add(Runnable object) {\n        synchronized (this) {\n            return super.add(object);\n        }\n    }\n\n    @Override\n    public boolean remove(Object object) {\n        synchronized (this) {\n            return super.remove(object);\n        }\n    }\n\n    @Override\n    public Runnable remove() {\n        synchronized (this) {\n            if (this.isEmpty())\n                return null;\n            return super.remove();\n        }\n    }\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/Util.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.util.Allocator;\nimport com.jeffmony.async.util.StreamUtility;\nimport com.jeffmony.async.wrapper.AsyncSocketWrapper;\nimport com.jeffmony.async.wrapper.DataEmitterWrapper;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\n\npublic class Util {\n    public static boolean SUPRESS_DEBUG_EXCEPTIONS = false;\n    public static void emitAllData(DataEmitter emitter, ByteBufferList list) {\n        int remaining;\n        DataCallback handler = null;\n        while (!emitter.isPaused() && (handler = emitter.getDataCallback()) != null && (remaining = list.remaining()) > 0) {\n            handler.onDataAvailable(emitter, list);\n            if (remaining == list.remaining() && handler == emitter.getDataCallback() && !emitter.isPaused()) {\n                // this is generally indicative of failure...\n\n                // 1) The data callback has not changed\n                // 2) no data was consumed\n                // 3) the data emitter was not paused\n\n                // call byteBufferList.recycle() or read all the data to prevent this assertion.\n                // this is nice to have, as it identifies protocol or parsing errors.\n\n//                System.out.println(\"Data: \" + list.peekString());\n                System.out.println(\"handler: \" + handler);\n                list.recycle();\n                if (SUPRESS_DEBUG_EXCEPTIONS)\n                    return;\n                assert false;\n                throw new RuntimeException(\"mDataHandler failed to consume data, yet remains the mDataHandler.\");\n            }\n        }\n        if (list.remaining() != 0 && !emitter.isPaused()) {\n            // not all the data was consumed...\n            // call byteBufferList.recycle() or read all the data to prevent this assertion.\n            // this is nice to have, as it identifies protocol or parsing errors.\n//            System.out.println(\"Data: \" + list.peekString());\n            System.out.println(\"handler: \" + handler);\n            System.out.println(\"emitter: \" + emitter);\n            list.recycle();\n            if (SUPRESS_DEBUG_EXCEPTIONS)\n                return;\n//            assert false;\n//            throw new AssertionError(\"Not all data was consumed by Util.emitAllData\");\n        }\n    }\n\n    public static void pump(final InputStream is, final DataSink ds, final CompletedCallback callback) {\n        pump(is, Integer.MAX_VALUE, ds, callback);\n    }\n\n    public static void pump(final InputStream is, final long max, final DataSink ds, final CompletedCallback callback) {\n        final CompletedCallback wrapper = new CompletedCallback() {\n            boolean reported;\n            @Override\n            public void onCompleted(Exception ex) {\n                if (reported)\n                    return;\n                reported = true;\n                callback.onCompleted(ex);\n            }\n        };\n\n        final WritableCallback cb = new WritableCallback() {\n            int totalRead = 0;\n            private void cleanup() {\n                ds.setClosedCallback(null);\n                ds.setWriteableCallback(null);\n                pending.recycle();\n                StreamUtility.closeQuietly(is);\n            }\n            ByteBufferList pending = new ByteBufferList();\n            Allocator allocator = new Allocator().setMinAlloc((int) Math.min(2 << 19, max));\n\n            @Override\n            public void onWriteable() {\n                try {\n                    do {\n                        if (!pending.hasRemaining()) {\n                            ByteBuffer b = allocator.allocate();\n\n                            long toRead = Math.min(max - totalRead, b.capacity());\n                            int read = is.read(b.array(), 0, (int)toRead);\n                            if (read == -1 || totalRead == max) {\n                                cleanup();\n                                wrapper.onCompleted(null);\n                                return;\n                            }\n                            allocator.track(read);\n                            totalRead += read;\n                            b.position(0);\n                            b.limit(read);\n                            pending.add(b);\n                        }\n                        \n                        ds.write(pending);\n                    }\n                    while (!pending.hasRemaining());\n                }\n                catch (Exception e) {\n                    cleanup();\n                    wrapper.onCompleted(e);\n                }\n            }\n        };\n        ds.setWriteableCallback(cb);\n\n        ds.setClosedCallback(wrapper);\n        \n        cb.onWriteable();\n    }\n    \n    public static void pump(final DataEmitter emitter, final DataSink sink, final CompletedCallback callback) {\n        final DataCallback dataCallback = new DataCallback() {\n            @Override\n            public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                sink.write(bb);\n                if (bb.remaining() > 0)\n                    emitter.pause();\n            }\n        };\n        emitter.setDataCallback(dataCallback);\n        sink.setWriteableCallback(new WritableCallback() {\n            @Override\n            public void onWriteable() {\n                emitter.resume();\n            }\n        });\n\n        final CompletedCallback wrapper = new CompletedCallback() {\n            boolean reported;\n            @Override\n            public void onCompleted(Exception ex) {\n                if (reported)\n                    return;\n                reported = true;\n                emitter.setDataCallback(null);\n                emitter.setEndCallback(null);\n                sink.setClosedCallback(null);\n                sink.setWriteableCallback(null);\n                callback.onCompleted(ex);\n            }\n        };\n\n        emitter.setEndCallback(wrapper);\n        sink.setClosedCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (ex == null)\n                    ex = new IOException(\"sink was closed before emitter ended\");\n                wrapper.onCompleted(ex);\n            }\n        });\n    }\n    \n    public static void stream(AsyncSocket s1, AsyncSocket s2, CompletedCallback callback) {\n        pump(s1, s2, callback);\n        pump(s2, s1, callback);\n    }\n    \n    public static void pump(final File file, final DataSink ds, final CompletedCallback callback) {\n        try {\n            if (file == null || ds == null) {\n                callback.onCompleted(null);\n                return;\n            }\n            final InputStream is = new FileInputStream(file);\n            pump(is, ds, new CompletedCallback() {\n                @Override\n                public void onCompleted(Exception ex) {\n                    try {\n                        is.close();\n                        callback.onCompleted(ex);\n                    }\n                    catch (IOException e) {\n                        callback.onCompleted(e);\n                    }\n                }\n            });\n        }\n        catch (Exception e) {\n            callback.onCompleted(e);\n        }\n    }\n\n    public static void writeAll(final DataSink sink, final ByteBufferList bb, final CompletedCallback callback) {\n        WritableCallback wc;\n        sink.setWriteableCallback(wc = new WritableCallback() {\n            @Override\n            public void onWriteable() {\n                sink.write(bb);\n                if (bb.remaining() == 0 && callback != null) {\n                    sink.setWriteableCallback(null);\n                    callback.onCompleted(null);\n                }\n            }\n        });\n        wc.onWriteable();\n    }\n    public static void writeAll(DataSink sink, byte[] bytes, CompletedCallback callback) {\n        ByteBuffer bb = ByteBufferList.obtain(bytes.length);\n        bb.put(bytes);\n        bb.flip();\n        ByteBufferList bbl = new ByteBufferList();\n        bbl.add(bb);\n        writeAll(sink, bbl, callback);\n    }\n\n    public static <T extends AsyncSocket> T getWrappedSocket(AsyncSocket socket, Class<T> wrappedClass) {\n        if (wrappedClass.isInstance(socket))\n            return (T)socket;\n        while (socket instanceof AsyncSocketWrapper) {\n            socket = ((AsyncSocketWrapper)socket).getSocket();\n            if (wrappedClass.isInstance(socket))\n                return (T)socket;\n        }\n        return null;\n    }\n\n    public static DataEmitter getWrappedDataEmitter(DataEmitter emitter, Class wrappedClass) {\n        if (wrappedClass.isInstance(emitter))\n            return emitter;\n        while (emitter instanceof DataEmitterWrapper) {\n            emitter = ((AsyncSocketWrapper)emitter).getSocket();\n            if (wrappedClass.isInstance(emitter))\n                return emitter;\n        }\n        return null;\n    }\n\n    public static void end(DataEmitter emitter, Exception e) {\n        if (emitter == null)\n            return;\n        end(emitter.getEndCallback(), e);\n    }\n\n    public static void end(CompletedCallback end, Exception e) {\n        if (end != null)\n            end.onCompleted(e);\n    }\n\n    public static void writable(DataSink emitter) {\n        if (emitter == null)\n            return;\n        writable(emitter.getWriteableCallback());\n    }\n\n    public static void writable(WritableCallback writable) {\n        if (writable != null)\n            writable.onWriteable();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/ZipDataSink.java",
    "content": "package com.jeffmony.async;\n\nimport com.jeffmony.async.callback.CompletedCallback;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\npublic class ZipDataSink extends FilteredDataSink {\n    public ZipDataSink(DataSink sink) {\n        super(sink);\n    }\n\n    ByteArrayOutputStream bout = new ByteArrayOutputStream();\n    ZipOutputStream zop = new ZipOutputStream(bout);\n\n    public void putNextEntry(ZipEntry ze) throws IOException {\n        zop.putNextEntry(ze);\n    }\n\n    public void closeEntry() throws IOException {\n        zop.closeEntry();\n    }\n    \n    protected void report(Exception e) {\n        CompletedCallback closed = getClosedCallback();\n        if (closed != null)\n            closed.onCompleted(e);\n    }\n\n    @Override\n    public void end() {\n        try {\n            zop.close();\n        }\n        catch (IOException e) {\n            report(e);\n            return;\n        }\n        setMaxBuffer(Integer.MAX_VALUE);\n        write(new ByteBufferList());\n        super.end();\n    }\n\n    @Override\n    public ByteBufferList filter(ByteBufferList bb) {\n        try {\n            if (bb != null) {\n                while (bb.size() > 0) {\n                    ByteBuffer b = bb.remove();\n                    ByteBufferList.writeOutputStream(zop, b);\n                    ByteBufferList.reclaim(b);\n                }\n            }\n            ByteBufferList ret = new ByteBufferList(bout.toByteArray());\n            bout.reset();\n            return ret;\n        }\n        catch (IOException e) {\n            report(e);\n            return null;\n        }\n        finally {\n            if (bb != null)\n                bb.recycle();\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/CompletedCallback.java",
    "content": "package com.jeffmony.async.callback;\n\npublic interface CompletedCallback {\n    class NullCompletedCallback implements CompletedCallback {\n        @Override\n        public void onCompleted(Exception ex) {\n\n        }\n    }\n\n    public void onCompleted(Exception ex);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ConnectCallback.java",
    "content": "package com.jeffmony.async.callback;\n\nimport com.jeffmony.async.AsyncSocket;\n\npublic interface ConnectCallback {\n    void onConnectCompleted(Exception ex, AsyncSocket socket);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ContinuationCallback.java",
    "content": "package com.jeffmony.async.callback;\n\nimport com.jeffmony.async.future.Continuation;\n\npublic interface ContinuationCallback {\n    void onContinue(Continuation continuation, CompletedCallback next) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/DataCallback.java",
    "content": "package com.jeffmony.async.callback;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\n\npublic interface DataCallback {\n    class NullDataCallback implements DataCallback {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            bb.recycle();\n        }\n    }\n\n    void onDataAvailable(DataEmitter emitter, ByteBufferList bb);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ListenCallback.java",
    "content": "package com.jeffmony.async.callback;\n\nimport com.jeffmony.async.AsyncServerSocket;\nimport com.jeffmony.async.AsyncSocket;\n\npublic interface ListenCallback extends CompletedCallback {\n    void onAccepted(AsyncSocket socket);\n    void onListening(AsyncServerSocket socket);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ResultCallback.java",
    "content": "package com.jeffmony.async.callback;\n\npublic interface ResultCallback<S, T> {\n    public void onCompleted(Exception e, S source, T result);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/SocketCreateCallback.java",
    "content": "package com.jeffmony.async.callback;\n\npublic interface SocketCreateCallback {\n    void onSocketCreated(int localPort);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ValueCallback.java",
    "content": "package com.jeffmony.async.callback;\n\n/**\n * Created by koush on 7/5/16.\n */\npublic interface ValueCallback<T> {\n    void onResult(T value);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/ValueFunction.java",
    "content": "package com.jeffmony.async.callback;\n\npublic interface ValueFunction<T> {\n    T getValue() throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/callback/WritableCallback.java",
    "content": "package com.jeffmony.async.callback;\n\npublic interface WritableCallback {\n    public void onWriteable();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/dns/Dns.java",
    "content": "package com.jeffmony.async.dns;\n\nimport com.jeffmony.async.AsyncDatagramSocket;\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.future.SimpleFuture;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.net.DatagramSocket;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Random;\n\n/**\n * Created by koush on 10/20/13.\n */\npublic class Dns {\n    public static Future<DnsResponse> lookup(String host) {\n        return lookup(AsyncServer.getDefault(), host, false, null);\n    }\n\n    private static int setFlag(int flags, int value, int offset) {\n        return flags | (value << offset);\n    }\n\n    private static int setQuery(int flags) {\n        return setFlag(flags, 0, 0);\n    }\n\n    private static int setRecursion(int flags) {\n        return setFlag(flags, 1, 8);\n    }\n\n    private static void addName(ByteBuffer bb, String name) {\n        String[] parts = name.split(\"\\\\.\");\n        for (String part: parts) {\n            bb.put((byte)part.length());\n            bb.put(part.getBytes());\n        }\n        bb.put((byte)0);\n    }\n\n    public static Future<DnsResponse> lookup(AsyncServer server, String host) {\n        return lookup(server, host, false, null);\n    }\n\n    public static Cancellable multicastLookup(AsyncServer server, String host, FutureCallback<DnsResponse> callback) {\n        return lookup(server, host, true, callback);\n    }\n\n    public static Cancellable multicastLookup(String host, FutureCallback<DnsResponse> callback) {\n        return multicastLookup(AsyncServer.getDefault(), host, callback);\n    }\n\n    public static Future<DnsResponse> lookup(AsyncServer server, String host, final boolean multicast, final FutureCallback<DnsResponse> callback) {\n        if (!server.isAffinityThread()) {\n            SimpleFuture<DnsResponse> ret = new SimpleFuture<>();\n            server.post(() -> ret.setComplete(lookup(server, host, multicast, callback)));\n            return ret;\n        }\n        ByteBuffer packet = ByteBufferList.obtain(1024).order(ByteOrder.BIG_ENDIAN);\n        short id = (short)new Random().nextInt();\n        short flags = (short)setQuery(0);\n        if (!multicast)\n            flags = (short)setRecursion(flags);\n\n        packet.putShort(id);\n        packet.putShort(flags);\n        // number questions\n        packet.putShort(multicast ? (short)1 : (short)2);\n        // number answer rr\n        packet.putShort((short)0);\n        // number authority rr\n        packet.putShort((short)0);\n        // number additional rr\n        packet.putShort((short)0);\n\n        addName(packet, host);\n        // query\n        packet.putShort(multicast ? (short)12 : (short)1);\n        // request internet address\n        packet.putShort((short)1);\n\n        if (!multicast) {\n            addName(packet, host);\n            // AAAA query\n            packet.putShort((short) 28);\n            // request internet address\n            packet.putShort((short)1);\n        }\n\n        packet.flip();\n\n\n        try {\n            final AsyncDatagramSocket dgram;\n            // todo, use the dns server...\n            if (!multicast) {\n                dgram = server.connectDatagram(new InetSocketAddress(\"8.8.8.8\", 53));\n            }\n            else {\n//                System.out.println(\"multicast dns...\");\n                dgram = AsyncServer.getDefault().openDatagram(null, 0, true);\n                Field field = DatagramSocket.class.getDeclaredField(\"impl\");\n                field.setAccessible(true);\n                Object impl = field.get(dgram.getSocket());\n                Method method = impl.getClass().getDeclaredMethod(\"join\", InetAddress.class);\n                method.setAccessible(true);\n                method.invoke(impl, InetAddress.getByName(\"224.0.0.251\"));\n                ((DatagramSocket)dgram.getSocket()).setBroadcast(true);\n            }\n            final SimpleFuture<DnsResponse> ret = new SimpleFuture<DnsResponse>() {\n                @Override\n                protected void cleanup() {\n                    super.cleanup();\n//                    System.out.println(\"multicast dns cleanup...\");\n                    dgram.close();\n                }\n            };\n            dgram.setDataCallback(new DataCallback() {\n                @Override\n                public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                    try {\n//                        System.out.println(dgram.getRemoteAddress());\n                        DnsResponse response = DnsResponse.parse(bb);\n//                        System.out.println(response);\n                        response.source = dgram.getRemoteAddress();\n\n                        if (!multicast) {\n                            dgram.close();\n                            ret.setComplete(response);\n                        }\n                        else {\n                            callback.onCompleted(null, response);\n                        }\n                    }\n                    catch (Exception e) {\n                    }\n                    bb.recycle();\n                }\n            });\n            if (!multicast)\n                dgram.write(new ByteBufferList(packet));\n            else\n                dgram.send(new InetSocketAddress(\"224.0.0.251\", 5353), packet);\n            return ret;\n        }\n        catch (Exception e) {\n            SimpleFuture<DnsResponse> ret = new SimpleFuture<DnsResponse>();\n            ret.setComplete(e);\n            if (multicast)\n                callback.onCompleted(e, null);\n            return ret;\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/dns/DnsResponse.java",
    "content": "package com.jeffmony.async.dns;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.http.Multimap;\n\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayList;\n\n/**\n * Created by koush on 10/20/13.\n */\npublic class DnsResponse {\n    public ArrayList<InetAddress> addresses = new ArrayList<InetAddress>();\n    public ArrayList<String> names = new ArrayList<String>();\n    public Multimap txt = new Multimap();\n    public InetSocketAddress source;\n\n    private static String parseName(ByteBufferList bb, ByteBuffer backReference) {\n        bb.order(ByteOrder.BIG_ENDIAN);\n        String ret = \"\";\n\n        int len;\n        while (0 != (len = bb.get() & 0x00FF)) {\n            // compressed\n            if ((len & 0x00c0) == 0x00c0) {\n                int offset = ((len & ~0xFFFFFFc0) << 8) | (bb.get() & 0x00FF);\n                if (ret.length() > 0)\n                    ret += \".\";\n                ByteBufferList sub = new ByteBufferList();\n                ByteBuffer duplicate = backReference.duplicate();\n                duplicate.get(new byte[offset]);\n                sub.add(duplicate);\n                return ret + parseName(sub, backReference);\n            }\n\n            byte[] bytes = new byte[len];\n            bb.get(bytes);\n            if (ret.length() > 0)\n                ret += \".\";\n            ret += new String(bytes);\n        }\n\n        return ret;\n    }\n\n    public static DnsResponse parse(ByteBufferList bb) {\n        ByteBuffer b = bb.getAll();\n        bb.add(b.duplicate());\n        // naive parsing...\n        bb.order(ByteOrder.BIG_ENDIAN);\n\n        // id\n        bb.getShort();\n        // flags\n        bb.getShort();\n\n        // number questions\n        int questions = bb.getShort();\n        // number answer rr\n        int answers = bb.getShort();\n        // number authority rr\n        int authorities = bb.getShort();\n        // number additional rr\n        int additionals = bb.getShort();\n\n        for (int i = 0; i < questions; i++) {\n            parseName(bb, b);\n            // type\n            bb.getShort();\n            // class\n            bb.getShort();\n        }\n\n        DnsResponse response = new DnsResponse();\n        for (int i = 0; i < answers; i++) {\n            String name = parseName(bb, b);\n            // type\n            int type = bb.getShort();\n            // class\n            int clazz = bb.getShort();\n            // ttl\n            int ttl = bb.getInt();\n            // length of address\n            int length = bb.getShort();\n            try {\n                if (type == 1) {\n                    // data\n                    byte[] data = new byte[length];\n                    bb.get(data);\n                    response.addresses.add(InetAddress.getByAddress(data));\n                }\n                else if (type == 0x000c) {\n                    response.names.add(parseName(bb, b));\n                }\n                else if (type == 16) {\n                    ByteBufferList txt = new ByteBufferList();\n                    bb.get(txt, length);\n                    response.parseTxt(txt);\n                }\n                else {\n                    bb.get(new byte[length]);\n                }\n            }\n            catch (Exception e) {\n//                e.printStackTrace();\n            }\n        }\n\n        // authorities\n        for (int i = 0; i < authorities; i++) {\n            String name = parseName(bb, b);\n            // type\n            int type = bb.getShort();\n            // class\n            int clazz = bb.getShort();\n            // ttl\n            int ttl = bb.getInt();\n            // length of address\n            int length = bb.getShort();\n            try {\n                bb.get(new byte[length]);\n            }\n            catch (Exception e) {\n//                e.printStackTrace();\n            }\n        }\n\n        // additionals\n        for (int i = 0; i < additionals; i++) {\n            String name = parseName(bb, b);\n            // type\n            int type = bb.getShort();\n            // class\n            int clazz = bb.getShort();\n            // ttl\n            int ttl = bb.getInt();\n            // length of address\n            int length = bb.getShort();\n            try {\n                if (type == 16) {\n                    ByteBufferList txt = new ByteBufferList();\n                    bb.get(txt, length);\n                    response.parseTxt(txt);\n                }\n                else {\n                    bb.get(new byte[length]);\n                }\n            }\n            catch (Exception e) {\n//                e.printStackTrace();\n            }\n        }\n\n        return response;\n    }\n\n    void parseTxt(ByteBufferList bb) {\n        while (bb.hasRemaining()) {\n            int length = (int)bb.get() & 0x00FF;\n            byte [] bytes = new byte[length];\n            bb.get(bytes);\n            String string = new String(bytes);\n            String[] pair = string.split(\"=\");\n            txt.add(pair[0], pair[1]);\n        }\n    }\n\n    @Override\n    public String toString() {\n        String ret = \"addresses:\\n\";\n        for (InetAddress address: addresses)\n            ret += address.toString() + \"\\n\";\n        ret += \"names:\\n\";\n        for (String name: names)\n            ret += name + \"\\n\";\n        return ret;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/Cancellable.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface Cancellable {\n    /**\n     * Check whether this asynchronous operation completed successfully.\n     * @return\n     */\n    boolean isDone();\n\n    /**\n     * Check whether this asynchronous operation has been cancelled.\n     * @return\n     */\n    boolean isCancelled();\n\n    /**\n     * Attempt to cancel this asynchronous operation.\n     * @return The return value is whether the operation cancelled successfully.\n     */\n    boolean cancel();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/Continuation.java",
    "content": "package com.jeffmony.async.future;\n\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ContinuationCallback;\n\nimport java.util.LinkedList;\n\npublic class Continuation extends SimpleCancellable implements ContinuationCallback, Runnable, Cancellable {\n    CompletedCallback callback;\n    Runnable cancelCallback;\n    \n    public CompletedCallback getCallback() {\n        return callback;\n    }\n    public void setCallback(CompletedCallback callback) {\n        this.callback = callback;\n    }\n    \n    public Runnable getCancelCallback() {\n        return cancelCallback;\n    }\n    public void setCancelCallback(Runnable cancelCallback) {\n        this.cancelCallback = cancelCallback;\n    }\n    public void setCancelCallback(final Cancellable cancel) {\n        if (cancel == null) {\n            this.cancelCallback = null;\n            return;\n        }\n        this.cancelCallback = new Runnable() {\n            @Override\n            public void run() {\n                cancel.cancel();\n            }\n        };\n    }\n    \n    public Continuation() {\n        this(null);\n    }\n    public Continuation(CompletedCallback callback) {\n        this(callback, null);\n    }\n    public Continuation(CompletedCallback callback, Runnable cancelCallback) {\n        this.cancelCallback = cancelCallback;\n        this.callback = callback;\n    }\n    \n    private CompletedCallback wrap() {\n        return new CompletedCallback() {\n            boolean mThisCompleted;\n            @Override\n            public void onCompleted(Exception ex) {\n                // onCompleted may be called more than once... buggy code.\n                // only accept the first (timeouts, etc)\n                if (mThisCompleted)\n                    return;\n                mThisCompleted = true;\n                assert waiting;\n                waiting = false;\n                if (ex == null) {\n                    next();\n                    return;\n                }\n                reportCompleted(ex);\n            }\n        };\n    }\n    \n    void reportCompleted(Exception ex) {\n        if (!setComplete())\n            return;\n        if (callback != null)\n            callback.onCompleted(ex);        \n    }\n    \n    LinkedList<ContinuationCallback> mCallbacks = new LinkedList<ContinuationCallback>();\n    \n    private ContinuationCallback hook(ContinuationCallback callback) {\n        if (callback instanceof DependentCancellable) {\n            DependentCancellable child = (DependentCancellable)callback;\n            child.setParent(this);\n        }\n        return callback;\n    }\n    \n    public Continuation add(ContinuationCallback callback) {\n        mCallbacks.add(hook(callback));\n        return this;\n    }\n    \n    public Continuation insert(ContinuationCallback callback) {\n        mCallbacks.add(0, hook(callback));\n        return this;\n    }\n   \n    public Continuation add(final DependentFuture future) {\n        future.setParent(this);\n        add(new ContinuationCallback() {\n            @Override\n            public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n                future.get();\n                next.onCompleted(null);\n            }\n        });\n        return this;\n    }\n    \n    private boolean inNext;\n    private boolean waiting;\n    private void next() {\n        if (inNext)\n            return;\n        while (mCallbacks.size() > 0 && !waiting && !isDone() && !isCancelled()) {\n            ContinuationCallback cb = mCallbacks.remove();\n            try {\n                inNext = true;\n                waiting = true;\n                cb.onContinue(this, wrap());\n            }\n            catch (Exception e) {\n                reportCompleted(e);\n            }\n            finally {\n                inNext = false;\n            }\n        }\n        if (waiting)\n            return;\n        if (isDone())\n            return;\n        if (isCancelled())\n            return;\n\n        reportCompleted(null);\n    }\n\n    @Override\n    public boolean cancel() {\n        if (!super.cancel())\n            return false;\n        \n        if (cancelCallback != null)\n            cancelCallback.run();\n        \n        return true;\n    }\n    \n    boolean started;\n    public Continuation start() {\n        if (started)\n            throw new IllegalStateException(\"already started\");\n        started = true;\n        next();\n        return this;\n    }\n\n    @Override\n    public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n        setCallback(next);\n        start();\n    }\n\n    @Override\n    public void run() {\n        start();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/Converter.java",
    "content": "package com.jeffmony.async.future;\n\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.ByteBufferList;\n\nimport org.json.JSONObject;\n\nimport java.io.InvalidObjectException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\n\npublic class Converter<R> {\n    public static <T> Converter<T> convert(Future<T> future, String mime) {\n        return new Converter<>(future, mime);\n    }\n\n    public static <T> Converter<T> convert(Future<T> future) {\n        return convert(future, null);\n    }\n\n    static class MimedData<T> {\n        public MimedData(T data, String mime) {\n            this.data = data;\n            this.mime = mime;\n        }\n        T data;\n        String mime;\n    }\n\n    static class MultiTransformer<T, F> extends MultiTransformFuture<MimedData<Future<T>>, MimedData<Future<F>>> {\n        TypeConverter<T, F> converter;\n        String converterMime;\n        int distance;\n        public MultiTransformer(TypeConverter<T, F> converter, String converterMime, int distance) {\n            this.converter = converter;\n            this.converterMime = converterMime;\n            this.distance = distance;\n        }\n\n        @Override\n        protected void transform(MimedData<Future<F>> converting) {\n            // transform will only ever be called once, and is called immediately,\n            // the transform is on the future itself, and not a pending value.\n            // so there's no risk of running the converter twice.\n            final String mime = converting.mime;\n\n            // this future will receive the eventual actual value.\n            final MultiFuture<T> converted = new MultiFuture<>();\n\n            // this marks the conversion as \"complete\". the conversion will start\n            // as soon as the value is ready.\n            setComplete(new MimedData<>(converted, mimeReplace(mime, converterMime)));\n\n            // wait on the incoming value and convert it\n            converting.data.thenConvert(data -> converter.convert(data, mime)).\n            setCallback((e, result1) -> {\n                if (e != null)\n                    converted.setComplete(e);\n                else\n                    converted.setComplete(result1);\n            });\n        }\n    }\n\n    static abstract class EnsureHashMap<K, V> extends LinkedHashMap<K, V> {\n        synchronized V ensure(K k) {\n            if (!containsKey(k)) {\n                put(k, makeDefault());\n            }\n            return get(k);\n        }\n\n        protected abstract V makeDefault();\n    }\n\n    static class MimedType<T> {\n        MimedType(Class<T> type, String mime) {\n            this.type = type;\n            this.mime = mime;\n        }\n        Class<T> type;\n        String mime;\n\n        @Override\n        public int hashCode() {\n            return type.hashCode() ^ mime.hashCode();\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            MimedType other = (MimedType)obj;\n            return type.equals(other.type) && mime.equals(other.mime);\n        }\n\n        // check if this mimed type is the same or more specific than this mimed type\n        public boolean isTypeOf(MimedType other) {\n            // check the type, this type must be less specific than the other type\n            if (!this.type.isAssignableFrom(other.type))\n                return false;\n\n            return isTypeOf(other.mime);\n        }\n\n        public String primary() {\n            return mime.split(\"/\")[0];\n        }\n\n        public String secondary() {\n            return mime.split(\"/\")[1];\n        }\n\n        // check if this mimed type is convertible to another mimed type\n        public boolean isTypeOf(String mime) {\n            String[] otherParts = mime.split(\"/\");\n            String[] myParts = this.mime.split(\"/\");\n\n            // ensure the other type is the same OR this type is fine with a wildcard\n            if (!\"*\".equals(myParts[0]) && !otherParts[0].equals(myParts[0]))\n                return false;\n\n            if (!\"*\".equals(myParts[1]) && !otherParts[1].equals(myParts[1]))\n                return false;\n\n            return true;\n        }\n\n        @Override\n        public String toString() {\n            return type.getSimpleName() + \" \" + mime;\n        }\n    }\n\n    static class ConverterTransformers<F, T> extends LinkedHashMap<MimedType<T>, MultiTransformer<T, F>> {\n    }\n\n    static class Converters<F, T> extends EnsureHashMap<MimedType<F>, ConverterTransformers<F, T>> {\n        @Override\n        protected ConverterTransformers makeDefault() {\n            return new ConverterTransformers();\n        }\n\n        private static <F, T> void add(ConverterTransformers<F, T> set, ConverterTransformers<F, T> more) {\n            if (more == null)\n                return;\n            set.putAll(more);\n        }\n        public ConverterTransformers<F, T> getAll(MimedType<T> mimedType) {\n            ConverterTransformers<F, T> ret = new ConverterTransformers<>();\n\n            for (MimedType candidate: keySet()) {\n                if (candidate.isTypeOf(mimedType))\n                    add(ret, get(candidate));\n            }\n\n            return ret;\n        }\n    }\n\n    Converters<Object, Object> outputs;\n\n    protected ConverterEntries getConverters() {\n        return new ConverterEntries(Converters);\n    }\n\n    MultiFuture<R> future = new MultiFuture<>();\n    String futureMime;\n    protected Converter(Future future, String mime) {\n        if (TextUtils.isEmpty(mime))\n            mime = MIME_ALL;\n        this.futureMime = mime;\n        this.future.setComplete(future);\n    }\n\n    synchronized private final <T> Future<T> to(Object value, Class<T> clazz, String mime) {\n        if (clazz.isInstance(value))\n            return new SimpleFuture<>((T) value);\n        return to(value.getClass(), clazz, mime);\n    }\n\n    synchronized private final <T> Future<T> to(Class fromClass, Class<T> clazz, String mime) {\n        if (TextUtils.isEmpty(mime))\n            mime = MIME_ALL;\n\n        if (outputs == null) {\n            outputs = new Converters<>();\n            ConverterEntries converters = getConverters();\n            for (ConverterEntry entry: converters.list) {\n                outputs.ensure(entry.from).put(entry.to, new MultiTransformer<>(entry.typeConverter, entry.to.mime, entry.distance));\n            }\n        }\n\n        MimedType<T> target = new MimedType<>(clazz, mime);\n        ArrayDeque<PathInfo> bestMatch = new ArrayDeque<>();\n        ArrayDeque<PathInfo> currentPath = new ArrayDeque<>();\n        if (search(target, bestMatch, currentPath, new MimedType(fromClass, futureMime), new HashSet<MimedType>())) {\n            PathInfo current = bestMatch.removeFirst();\n\n            new SimpleFuture<>(new MimedData<>((Future<Object>)future, futureMime)).setCallback(current.transformer);\n\n            while (!bestMatch.isEmpty()) {\n                PathInfo next = bestMatch.removeFirst();\n                current.transformer.setCallback(next.transformer);\n                current = next;\n            }\n\n            return ((MultiTransformer<T, Object>)current.transformer).then(from -> from.data);\n        }\n\n        return new SimpleFuture<>(new InvalidObjectException(\"unable to find converter\"));\n    }\n\n    static class PathInfo {\n        MultiTransformer<Object, Object> transformer;\n        String mime;\n        MimedType candidate;\n\n        static int distance(ArrayDeque<PathInfo> path) {\n            int distance = 0;\n            for (PathInfo entry: path) {\n                distance += entry.transformer.distance;\n            }\n            return distance;\n        }\n    }\n\n    static String mimeReplace(String mime1, String mime2) {\n        String[] parts = mime2.split(\"/\");\n        String[] myParts = mime1.split(\"/\");\n\n        // a wildcard mime converter adopts the mime of the converted type\n        String primary = !\"*\".equals(parts[0]) ? parts[0] : myParts[0];\n        String secondary = !\"*\".equals(parts[1]) ? parts[1] : myParts[1];\n\n        return primary + \"/\" + secondary;\n    }\n\n    public final <T> Future<T> to(Class<T> clazz) {\n        return to(clazz, null);\n    }\n\n    private <T> boolean search(MimedType<T> target, ArrayDeque<PathInfo> bestMatch, ArrayDeque<PathInfo> currentPath, MimedType currentSearch, HashSet<MimedType> searched) {\n        if (target.isTypeOf(currentSearch)) {\n            bestMatch.clear();\n            bestMatch.addAll(currentPath);\n            return true;\n        }\n\n        // the current path must have potential to be better than the best match\n        if (!bestMatch.isEmpty() && PathInfo.distance(currentPath) >= PathInfo.distance(bestMatch))\n            return false;\n\n        // prevent reentrancy\n        if (searched.contains(currentSearch))\n            return false;\n\n        boolean found = false;\n        searched.add(currentSearch);\n        ConverterTransformers<Object, Object> converterTransformers = outputs.getAll(currentSearch);\n        for (MimedType candidate: converterTransformers.keySet()) {\n            // this simulates the mime results of a transform\n            MimedType newSearch = new MimedType(candidate.type, mimeReplace(currentSearch.mime, candidate.mime));\n\n            PathInfo path = new PathInfo();\n            path.transformer = converterTransformers.get(candidate);\n            path.mime = newSearch.mime;\n            path.candidate = candidate;\n            currentPath.addLast(path);\n            try {\n                found |= search(target, bestMatch, currentPath, newSearch, searched);\n            }\n            finally {\n                currentPath.removeLast();\n            }\n        }\n\n        if (found) {\n            // if this resulted in a success,\n            // clear this from the currentSearch list, because we know this leads\n            // to a potential solution. maybe we can arrive here faster.\n            searched.remove(currentSearch);\n        }\n\n        return found;\n    }\n\n    private static final String MIME_ALL = \"*/*\";\n    public <T> Future<T> to(Class<T> clazz, String mime) {\n        return future.then(from -> to(from, clazz, mime));\n    }\n\n    static class ConverterEntry<F, T> {\n        ConverterEntry(Class<F> from, String fromMime, Class<T> to, String toMime, int distance, TypeConverter<T, F> typeConverter) {\n            this.from = new MimedType<>(from, fromMime);\n            this.to = new MimedType<>(to, toMime);\n            this.distance = distance;\n            this.typeConverter = typeConverter;\n        }\n        MimedType<F> from;\n        MimedType<T> to;\n        int distance;\n        TypeConverter<T, F> typeConverter;\n\n        @Override\n        public int hashCode() {\n            return from.hashCode() ^ to.hashCode();\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            ConverterEntry other = (ConverterEntry)obj;\n            return from.equals(other.from) && to.equals(other.to);\n        }\n    }\n\n    public static class ConverterEntries {\n        public ArrayList<ConverterEntry> list = new ArrayList<>();\n        public ConverterEntries() {\n        }\n\n        public ConverterEntries(ConverterEntries other) {\n            list.addAll(other.list);\n        }\n\n        public synchronized <F, T> void addConverter(Class<F> from, String fromMime, Class<T> to, String toMime, TypeConverter<T, F> typeConverter) {\n            addConverter(from, fromMime, to, toMime, 1, typeConverter);\n        }\n        public synchronized <F, T> void addConverter(Class<F> from, String fromMime, Class<T> to, String toMime, int distance, TypeConverter<T, F> typeConverter) {\n            if (TextUtils.isEmpty(fromMime))\n                fromMime = MIME_ALL;\n            if (TextUtils.isEmpty(toMime))\n                toMime = MIME_ALL;\n\n            list.add(new ConverterEntry<>(from, fromMime, to, toMime, distance, typeConverter));\n        }\n\n        public synchronized boolean removeConverter(TypeConverter typeConverter) {\n            for (ConverterEntry entry: list) {\n                if (entry.typeConverter == typeConverter)\n                    return list.remove(entry);\n            }\n            return false;\n        }\n    }\n\n    public final static ConverterEntries Converters = new ConverterEntries();\n\n    static {\n        // ensure byte buffer operations are idempotent. do deep copies.\n        final TypeConverter<ByteBufferList, byte[]> ByteArrayToByteBufferList = (from, fromMime) ->\n                new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(ByteBuffer.wrap(from))));\n        final TypeConverter<byte[], ByteBufferList> ByteBufferListToByteArray = (from, fromMime) ->\n                new SimpleFuture<>(from.getAllByteArray());\n        final TypeConverter<ByteBuffer, ByteBufferList> ByteBufferListToByteBuffer = (from, fromMime) ->\n                new SimpleFuture<>(from.getAll());\n        final TypeConverter<String, ByteBufferList> ByteBufferListToString = (from, fromMime) ->\n                new SimpleFuture<>(from.peekString());\n        final TypeConverter<ByteBuffer, byte[]> ByteArrayToByteBuffer = (from, fromMime) ->\n                new SimpleFuture<>(ByteBufferList.deepCopy(ByteBuffer.wrap(from)));\n        final TypeConverter<ByteBufferList, ByteBuffer> ByteBufferToByteBufferList = (from, fromMime) ->\n                new SimpleFuture<>(new ByteBufferList(ByteBufferList.deepCopy(from)));\n\n        final TypeConverter<byte[], String> StringToByteArray = (from, fromMime) -> new SimpleFuture<>(from.getBytes());\n        final TypeConverter<JSONObject, String> StringToJSONObject = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::new);\n        final TypeConverter<String, JSONObject> JSONObjectToString = (from, fromMime) -> new SimpleFuture<>(from).thenConvert(JSONObject::toString);\n        final TypeConverter<String, byte[]> ByteArrayToString = (from, fromMime) -> new SimpleFuture<>(new String(from));\n\n        Converters.addConverter(ByteBuffer.class, null, ByteBufferList.class, null, ByteBufferToByteBufferList);\n        Converters.addConverter(String.class, null, byte[].class, \"text/plain\", StringToByteArray);\n        Converters.addConverter(byte[].class, null, ByteBufferList.class, null, ByteArrayToByteBufferList);\n        Converters.addConverter(ByteBufferList.class, null, byte[].class, null, ByteBufferListToByteArray);\n        Converters.addConverter(ByteBufferList.class, null, ByteBuffer.class, null, ByteBufferListToByteBuffer);\n        Converters.addConverter(ByteBufferList.class, \"text/plain\", String.class, null, ByteBufferListToString);\n        Converters.addConverter(byte[].class, null, ByteBuffer.class, null, ByteArrayToByteBuffer);\n        Converters.addConverter(String.class, \"application/json\", JSONObject.class, null, StringToJSONObject);\n        Converters.addConverter(JSONObject.class, null, String.class, \"application/json\", JSONObjectToString);\n        Converters.addConverter(byte[].class, \"text/plain\", String.class, null, ByteArrayToString);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/DependentCancellable.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface DependentCancellable extends Cancellable {\n    boolean setParent(Cancellable parent);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/DependentFuture.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface DependentFuture<T> extends Future<T>, DependentCancellable {\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/DoneCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface DoneCallback<T> {\n    void done(Exception e, T result) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FailCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface FailCallback {\n    /**\n     * Callback that is invoked when a future completes with an error.\n     * The error should be rethrown to pass it along.\n     * @param e\n     * @throws Exception\n     */\n    void fail(Exception e) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FailConvertCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface FailConvertCallback<T> {\n    /**\n     * Callback that is invoked when a future completes with an error.\n     * The error should be rethrown, or a new value should be returned.\n     * @param e\n     * @return\n     * @throws Exception\n     */\n    T fail(Exception e) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FailRecoverCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface FailRecoverCallback<T> {\n    /**\n     * Callback that is invoked when a future completes with an error.\n     * The error should be rethrown, or a new future value should be returned.\n     * @param e\n     * @return\n     * @throws Exception\n     */\n    Future<T> fail(Exception e) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/Future.java",
    "content": "package com.jeffmony.async.future;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport java.util.concurrent.Executor;\n\npublic interface Future<T> extends Cancellable, java.util.concurrent.Future<T> {\n    /**\n     * Set a callback to be invoked when this Future completes.\n     * @param callback\n     * @return\n     */\n    void setCallback(FutureCallback<T> callback);\n\n    /**\n     * Set a callback to be invoked when the Future completes\n     * with an error or a result.\n     * The existing error or result will be passed down the chain, or a new error\n     * may be thrown.\n     * @param done\n     * @return\n     */\n    Future<T> done(DoneCallback<T> done);\n\n    /**\n     * Set a callback to be invoked when this Future completes successfully.\n     * @param callback\n     * @return A future that will resolve once the success callback completes,\n     * which may contain any errors thrown by the success callback.\n     */\n    Future<T> success(SuccessCallback<T> callback);\n\n    /**\n     * Set a callback to be invoked when this Future completes successfully.\n     * @param then\n     * @param <R>\n     * @return A future containing all exceptions that happened prior or during\n     * the callback, or the successful result.\n     */\n    <R> Future<R> then(ThenFutureCallback<R, T> then);\n\n    /**\n     * Set a callback to be invoked when this Future completes successfully.\n     * @param then\n     * @param <R>\n     * @return A future containing all exceptions that happened prior or during\n     * the callback, or the successful result.\n     */\n    <R> Future<R> thenConvert(ThenCallback<R, T> then);\n\n    /**\n     * Set a callback to be invoked when this future completes with a failure.\n     * The failure can be observered and rethrown, otherwise it is considered handled.\n     * The exception will be nulled for subsequent callbacks in the chain.\n     * @param fail\n     * @return\n     */\n    Future<T> fail(FailCallback fail);\n\n    /**\n     * Set a callback to be invoked when this future completes with a failure.\n     * The failure can be observered and rethrown, or handled by returning\n     * a new fallback value of the same type.\n     * @param fail\n     * @return\n     */\n    Future<T> failConvert(FailConvertCallback<T> fail);\n\n    /**\n     * Set a callback to be invoked when this future completes with a failure.\n     * The failure should be observered and rethrown, or handled by returning\n     * a new future of the same type.\n     * @param fail\n     * @return\n     */\n    Future<T> failRecover(FailRecoverCallback<T> fail);\n\n    /**\n     * Get the result, if any. Returns null if still in progress.\n     * @return\n     */\n    T tryGet();\n\n    /**\n     * Get the exception, if any. Returns null if still in progress.\n     * @return\n     */\n    Exception tryGetException();\n\n    /**\n     * Get the result on the executor thread.\n     * @param executor\n     * @return\n     */\n    @RequiresApi(api = Build.VERSION_CODES.N)\n    default Future<T> executorThread(Executor executor) {\n        SimpleFuture<T> ret = new SimpleFuture<>();\n        executor.execute(() -> ret.setComplete(Future.this));\n        return ret;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FutureCallback.java",
    "content": "package com.jeffmony.async.future;\n\n/**\n * Created by koush on 5/20/13.\n */\npublic interface FutureCallback<T> {\n    /**\n     * onCompleted is called by the Future with the result or exception of the asynchronous operation.\n     * @param e Exception encountered by the operation\n     * @param result Result returned from the operation\n     */\n    void onCompleted(Exception e, T result);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FutureRunnable.java",
    "content": "package com.jeffmony.async.future;\n\n/**\n * Created by koush on 12/22/13.\n */\npublic interface FutureRunnable<T> {\n    T run() throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/FutureThread.java",
    "content": "package com.jeffmony.async.future;\n\nimport java.util.concurrent.ExecutorService;\n\n/**\n * Created by koush on 12/22/13.\n */\npublic class FutureThread<T> extends SimpleFuture<T> {\n    public FutureThread(final FutureRunnable<T> runnable) {\n        this(runnable, \"FutureThread\");\n    }\n\n    public FutureThread(final ExecutorService pool, final FutureRunnable<T> runnable) {\n        pool.submit(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    setComplete(runnable.run());\n                }\n                catch (Exception e) {\n                    setComplete(e);\n                }\n            }\n        });\n    }\n\n    public FutureThread(final FutureRunnable<T> runnable, String name) {\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    setComplete(runnable.run());\n                }\n                catch (Exception e) {\n                    setComplete(e);\n                }\n            }\n        }, name).start();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/Futures.java",
    "content": "package com.jeffmony.async.future;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\n\npublic class Futures {\n    public static <T> Future<List<T>> waitAll(final List<Future<T>> futures) {\n        final ArrayList<T> results = new ArrayList<>();\n        final SimpleFuture<List<T>> ret = new SimpleFuture<>();\n\n        if (futures.isEmpty()) {\n            ret.setComplete(results);\n            return ret;\n        }\n\n        FutureCallback<T> cb = new FutureCallback<T>() {\n            int count = 0;\n\n            @Override\n            public void onCompleted(Exception e, T result) {\n                results.add(result);\n                count++;\n                if (count < futures.size())\n                    futures.get(count).setCallback(this);\n                else\n                    ret.setComplete(results);\n            }\n        };\n\n        futures.get(0).setCallback(cb);\n\n        return ret;\n    }\n\n    public static <T> Future<List<T>> waitAll(final Future<T>... futures) {\n        return waitAll(Arrays.asList(futures));\n    }\n\n\n    private static <T, F> void loopUntil(final Iterator<F> values, ThenFutureCallback<T, F> callback, SimpleFuture<T> ret, Exception lastException) {\n        while (values.hasNext()) {\n            try {\n                callback.then(values.next())\n                        .success(ret::setComplete)\n                        .fail(e -> loopUntil(values, callback, ret, e));\n                return;\n            } catch (Exception e) {\n                lastException = e;\n            }\n        }\n\n        if (lastException == null)\n            ret.setComplete(new Exception(\"empty list\"));\n        else\n            ret.setComplete(lastException);\n    }\n\n    public static <T, F> Future<T> loopUntil(final Iterable<F> values, ThenFutureCallback<T, F> callback) {\n        SimpleFuture<T> ret = new SimpleFuture<>();\n        loopUntil(values.iterator(), callback, ret, null);\n        return ret;\n    }\n\n    public static <T, F> Future<T> loopUntil(final F[] values, ThenFutureCallback<T, F> callback) {\n        return loopUntil(Arrays.asList(values), callback);\n    }\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/HandlerFuture.java",
    "content": "package com.jeffmony.async.future;\n\nimport android.os.Handler;\nimport android.os.Looper;\n\n/**\n * Created by koush on 12/25/13.\n */\npublic class HandlerFuture<T> extends SimpleFuture<T> {\n    Handler handler;\n\n    public HandlerFuture() {\n        Looper looper = Looper.myLooper();\n        if (looper == null)\n            looper = Looper.getMainLooper();\n        handler = new Handler(looper);\n    }\n\n    @Override\n    public void setCallback(final FutureCallback<T> callback) {\n        FutureCallback<T> wrapped = new FutureCallback<T>() {\n            @Override\n            public void onCompleted(final Exception e, final T result) {\n                if (Looper.myLooper() == handler.getLooper()) {\n                    callback.onCompleted(e, result);\n                    return;\n                }\n\n                handler.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        onCompleted(e, result);\n                    }\n                });\n            }\n        };\n        super.setCallback(wrapped);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/MultiFuture.java",
    "content": "package com.jeffmony.async.future;\n\nimport java.util.ArrayList;\n\n/**\n * Created by koush on 2/25/14.\n */\npublic class MultiFuture<T> extends SimpleFuture<T> {\n    private ArrayList<FutureCallbackInternal<T>> internalCallbacks;\n\n    public MultiFuture() {\n    }\n\n    public MultiFuture(T value) {\n        super(value);\n    }\n\n    public MultiFuture(Exception e) {\n        super(e);\n    }\n\n    public MultiFuture(Future<T> future) {\n        super(future);\n    }\n\n    private final FutureCallbackInternal<T> internalCallback = (e, result, callsite) -> {\n        ArrayList<FutureCallbackInternal<T>> callbacks;\n        synchronized (MultiFuture.this) {\n            callbacks = MultiFuture.this.internalCallbacks;\n            MultiFuture.this.internalCallbacks = null;\n        }\n\n        if (callbacks == null)\n            return;\n        for (FutureCallbackInternal<T> cb : callbacks) {\n            cb.onCompleted(e, result, callsite);\n        }\n    };\n\n    @Override\n    protected void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal<T> internalCallback) {\n        synchronized (this) {\n            if (internalCallback != null) {\n                if (internalCallbacks == null)\n                    internalCallbacks = new ArrayList<>();\n                internalCallbacks.add(internalCallback);\n            }\n        }\n        // so, there is a race condition where this internal callback could get\n        // executed twice, if two callbacks are added at the same time.\n        // however, it doesn't matter, as the actual retrieval and nulling\n        // of the callback list is done in another sync block.\n        // one of the invocations will actually invoke all the callbacks,\n        // while the other will not get a list back.\n\n        // race:\n        // 1-ADD\n        // 2-ADD\n        // 1-INVOKE LIST\n        // 2-INVOKE NULL\n\n        super.setCallbackInternal(callsite, this.internalCallback);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/MultiTransformFuture.java",
    "content": "package com.jeffmony.async.future;\n\npublic abstract class MultiTransformFuture<T, F> extends MultiFuture<T> implements FutureCallback<F> {\n    @Override\n    public void onCompleted(Exception e, F result) {\n        if (isCancelled())\n            return;\n        if (e != null) {\n            error(e);\n            return;\n        }\n\n        try {\n            transform(result);\n        }\n        catch (Exception ex) {\n            error(ex);\n        }\n    }\n\n    protected void error(Exception e) {\n        setComplete(e);\n    }\n\n    protected abstract void transform(F result) throws Exception;\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/SimpleCancellable.java",
    "content": "package com.jeffmony.async.future;\n\npublic class SimpleCancellable implements DependentCancellable {\n    boolean complete;\n    @Override\n    public boolean isDone() {\n        return complete;\n    }\n\n    protected void cancelCleanup() {\n    }\n\n    protected void cleanup() {\n    }\n\n    protected void completeCleanup() {\n    }\n\n    public boolean setComplete() {\n        synchronized (this) {\n            if (cancelled)\n                return false;\n            if (complete) {\n                // don't allow a Cancellable to complete twice...\n                return false;\n            }\n            complete = true;\n            parent = null;\n        }\n        completeCleanup();\n        cleanup();\n        return true;\n    }\n\n    @Override\n    public boolean cancel() {\n        Cancellable parent;\n        synchronized (this) {\n            if (complete)\n                return false;\n            if (cancelled)\n                return true;\n            cancelled = true;\n            parent = this.parent;\n            // null out the parent to allow garbage collection\n            this.parent = null;\n        }\n        if (parent != null)\n            parent.cancel();\n        cancelCleanup();\n        cleanup();\n        return true;\n    }\n    boolean cancelled;\n\n    private Cancellable parent;\n    @Override\n    public boolean setParent(Cancellable parent) {\n        synchronized (this) {\n            if (isDone())\n                return false;\n            this.parent = parent;\n            return true;\n        }\n    }\n\n    @Override\n    public boolean isCancelled() {\n        synchronized (this) {\n            return cancelled || (parent != null && parent.isCancelled());\n        }\n    }\n\n    public static final Cancellable COMPLETED = new SimpleCancellable() {\n        {\n            setComplete();\n        }\n    };\n\n    public static final Cancellable CANCELLED = new SimpleCancellable() {\n        {\n            cancel();\n        }\n    };\n\n    public Cancellable reset() {\n        cancel();\n        complete = false;\n        cancelled = false;\n        return this;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/SimpleFuture.java",
    "content": "package com.jeffmony.async.future;\n\nimport com.jeffmony.async.AsyncSemaphore;\n\nimport java.util.concurrent.CancellationException;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\npublic class SimpleFuture<T> extends SimpleCancellable implements DependentFuture<T> {\n    private AsyncSemaphore waiter;\n    private Exception exception;\n    private T result;\n    private boolean silent;\n    private FutureCallbackInternal<T> internalCallback;\n\n    protected interface FutureCallbackInternal<T> {\n        void onCompleted(Exception e, T result, FutureCallsite next);\n    }\n\n    public SimpleFuture() {\n    }\n\n    public SimpleFuture(T value) {\n        setComplete(value);\n    }\n\n    public SimpleFuture(Exception e) {\n        setComplete(e);\n    }\n\n    public SimpleFuture(Future<T> future) {\n        setComplete(future);\n    }\n\n    @Override\n    public boolean cancel(boolean mayInterruptIfRunning) {\n        return cancel();\n    }\n\n    private boolean cancelInternal(boolean silent) {\n        if (!super.cancel())\n            return false;\n        // still need to release any pending waiters\n        FutureCallbackInternal<T> internalCallback;\n        synchronized (this) {\n            exception = new CancellationException();\n            releaseWaiterLocked();\n            internalCallback = handleInternalCompleteLocked();\n            this.silent = silent;\n        }\n        handleCallbackUnlocked(null, internalCallback);\n        return true;\n    }\n\n    public boolean cancelSilently() {\n        return cancelInternal(true);\n    }\n\n    @Override\n    public boolean cancel() {\n        return cancelInternal(silent);\n    }\n\n    @Override\n    public T get() throws InterruptedException, ExecutionException {\n        AsyncSemaphore waiter;\n        synchronized (this) {\n            if (isCancelled() || isDone())\n                return getResultOrThrow();\n            waiter = ensureWaiterLocked();\n        }\n        waiter.acquire();\n        return getResultOrThrow();\n    }\n\n    private T getResultOrThrow() throws ExecutionException {\n        if (exception != null)\n            throw new ExecutionException(exception);\n        return result;\n    }\n\n    @Override\n    public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {\n        AsyncSemaphore waiter;\n        synchronized (this) {\n            if (isCancelled() || isDone())\n                return getResultOrThrow();\n            waiter = ensureWaiterLocked();\n        }\n        if (!waiter.tryAcquire(timeout, unit))\n            throw new TimeoutException();\n        return getResultOrThrow();\n    }\n\n    @Override\n    public boolean setComplete() {\n        return setComplete((T)null);\n    }\n\n    private FutureCallbackInternal<T> handleInternalCompleteLocked() {\n        // don't execute the callback inside the sync block... possible hangup\n        // read the callback value, and then call it outside the block.\n        // can't simply call this.callback.onCompleted directly outside the block,\n        // because that may result in a race condition where the callback changes once leaving\n        // the block.\n        FutureCallbackInternal<T> callback = this.internalCallback;\n        // null out members to allow garbage collection\n        this.internalCallback = null;\n        return callback;\n    }\n\n    static class FutureCallsite {\n        Exception e;\n        Object result;\n        FutureCallbackInternal callback;\n\n        void loop() {\n            while (callback != null) {\n                // these values always start non null.\n                FutureCallbackInternal callback = this.callback;\n                Exception e = this.e;\n                Object result = this.result;\n\n                // null them out for reentrancy\n                this.callback = null;\n                this.e = null;\n                this.result = null;\n\n                callback.onCompleted(e, result, this);\n            }\n        }\n    }\n\n    private void handleCallbackUnlocked(FutureCallsite callsite, FutureCallbackInternal<T> internalCallback) {\n        if (silent)\n            return;\n\n        if (internalCallback == null)\n            return;\n\n        boolean needsLoop = false;\n        if (callsite == null) {\n            needsLoop = true;\n            callsite = new FutureCallsite();\n        }\n\n        callsite.callback = internalCallback;\n        callsite.e = exception;\n        callsite.result = result;\n\n        if (needsLoop)\n            callsite.loop();\n    }\n\n    void releaseWaiterLocked() {\n        if (waiter != null) {\n            waiter.release();\n            waiter = null;\n        }\n    }\n\n    AsyncSemaphore ensureWaiterLocked() {\n        if (waiter == null)\n            waiter = new AsyncSemaphore();\n        return waiter;\n    }\n\n    public boolean setComplete(Exception e) {\n        return setComplete(e, null, null);\n    }\n    public boolean setCompleteException(Exception e) { return setComplete(e, null, null); }\n\n    public boolean setComplete(T value) {\n        return setComplete(null, value, null);\n    }\n    public boolean setCompleteValue(T value) {\n        return setComplete(null, value, null);\n    }\n\n    public boolean setComplete(Exception e, T value) {\n        return setComplete(e, value, null);\n    }\n\n    private boolean setComplete(Exception e, T value, FutureCallsite callsite) {\n        FutureCallbackInternal<T> internalCallback;\n        synchronized (this) {\n            if (!super.setComplete())\n                return false;\n            result = value;\n            exception = e;\n            releaseWaiterLocked();\n            internalCallback = handleInternalCompleteLocked();\n        }\n        handleCallbackUnlocked(callsite, internalCallback);\n        return true;\n    }\n\n    void setCallbackInternal(FutureCallsite callsite, FutureCallbackInternal<T> internalCallback) {\n        // callback can only be changed or read/used inside a sync block\n        synchronized (this) {\n            this.internalCallback = internalCallback;\n            if (!isDone() && !isCancelled())\n                return;\n\n            internalCallback = handleInternalCompleteLocked();\n        }\n        handleCallbackUnlocked(callsite, internalCallback);\n    }\n\n    @Override\n    public void setCallback(FutureCallback<T> callback) {\n        if (callback == null)\n            setCallbackInternal(null, null);\n        else\n            setCallbackInternal(null, (e, result, next) -> callback.onCompleted(e, result));\n    }\n\n    private Future<T> setComplete(Future<T> future, FutureCallsite callsite) {\n        setParent(future);\n\n        SimpleFuture<T> ret = new SimpleFuture<>();\n        if (future instanceof SimpleFuture) {\n            ((SimpleFuture<T>)future).setCallbackInternal(callsite,\n                    (e, result, next) ->\n                            ret.setComplete(SimpleFuture.this.setComplete(e, result, next) ? null : new CancellationException(), result, next));\n        }\n        else {\n            future.setCallback((e, result) -> ret.setComplete(SimpleFuture.this.setComplete(e, result, null) ? null : new CancellationException()));\n        }\n        return ret;\n    }\n\n    /**\n     * Complete a future with another future. Returns a future that reports whether the completion\n     * was successful. If the future was not completed due to cancellation, the callback\n     * will be called with a CancellationException, and the original future result, if one was provided.\n     * @param future\n     * @return\n     */\n    public Future<T> setComplete(Future<T> future) {\n        return setComplete(future, null);\n    }\n\n    public Future<T> setCompleteFuture(Future<T> future) {\n        return setComplete(future, null);\n    }\n\n\n    /**\n     * THIS METHOD IS FOR TEST USE ONLY\n     * @return\n     */\n    @Deprecated\n    public Object getCallback() {\n        return internalCallback;\n    }\n\n    @Override\n    public Future<T> done(DoneCallback<T> done) {\n        final SimpleFuture<T> ret = new SimpleFuture<>();\n        ret.setParent(this);\n        setCallbackInternal(null, (e, result, next) -> {\n            if (e == null) {\n                try {\n                    done.done(e, result);\n                }\n                catch (Exception callbackException) {\n                    e = callbackException;\n                    // note that the result is not nulled out. this is useful for managed resources, like sockets.\n                    // for example: a successful socket connection was made, but the request can be cancelled.\n                    // so, returning an error along with a socket object allows for failure cleanup.\n                }\n            }\n            ret.setComplete(e, result, next);\n        });\n        return ret;\n    }\n\n    @Override\n    public Future<T> success(SuccessCallback<T> callback) {\n        final SimpleFuture<T> ret = new SimpleFuture<>();\n        ret.setParent(this);\n        setCallbackInternal(null, (e, result, next) -> {\n            if (e == null) {\n                try {\n                    callback.success(result);\n                }\n                catch (Exception callbackException) {\n                    e = callbackException;\n                    // note that the result is not nulled out. this is useful for managed resources, like sockets.\n                    // for example: a successful socket connection was made, but the request can be cancelled.\n                    // so, returning an error along with a socket object allows for failure cleanup.\n                }\n            }\n            ret.setComplete(e, result, next);\n        });\n        return ret;\n    }\n\n    @Override\n    public <R> Future<R> then(ThenFutureCallback<R, T> then) {\n        final SimpleFuture<R> ret = new SimpleFuture<>();\n        ret.setParent(this);\n        setCallbackInternal(null, (e, result, next) -> {\n            if (e != null) {\n                ret.setComplete(e, null, next);\n                return;\n            }\n            Future<R> out;\n            try {\n                out = then.then(result);\n            }\n            catch (Exception callbackException) {\n                ret.setComplete(callbackException, null, next);\n                return;\n            }\n            ret.setComplete(out, next);\n\n        });\n        return ret;\n    }\n\n    @Override\n    public <R> Future<R> thenConvert(final ThenCallback<R, T> callback) {\n        return then(from -> new SimpleFuture<>(callback.then(from)));\n    }\n\n    @Override\n    public Future<T> fail(FailCallback fail) {\n        return failRecover(e -> {\n            fail.fail(e);\n            return new SimpleFuture<>((T)null);\n        });\n    }\n\n    @Override\n    public Future<T> failRecover(FailRecoverCallback<T> fail) {\n        SimpleFuture<T> ret = new SimpleFuture<>();\n        ret.setParent(this);\n        setCallbackInternal(null, (e, result, next) -> {\n            if (e == null) {\n                ret.setComplete(e, result, next);\n                return;\n            }\n            Future<T> out;\n            try {\n                out = fail.fail(e);\n            }\n            catch (Exception callbackException) {\n                ret.setComplete(callbackException, null, next);\n                return;\n            }\n            ret.setComplete(out, next);\n        });\n        return ret;\n    }\n\n    @Override\n    public Future<T> failConvert(FailConvertCallback<T> fail) {\n        return failRecover(e -> new SimpleFuture<>(fail.fail(e)));\n    }\n\n    @Override\n    public boolean setParent(Cancellable parent) {\n        return super.setParent(parent);\n    }\n\n    /**\n     * Reset the future for reuse.\n     * @return\n     */\n    public SimpleFuture<T> reset() {\n        super.reset();\n\n        result = null;\n        exception = null;\n        waiter = null;\n        internalCallback = null;\n        silent = false;\n\n        return this;\n    }\n\n    @Override\n    public Exception tryGetException() {\n        return exception;\n    }\n\n    @Override\n    public T tryGet() {\n        return result;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/SuccessCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface SuccessCallback<T> {\n    void success(T value) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/ThenCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface ThenCallback<T, F> {\n    /**\n     * Callback that is invoked when Future.then completes,\n     * and converts a value F to value T.\n     * @param from\n     * @return\n     * @throws Exception\n     */\n    T then(F from) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/ThenFutureCallback.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface ThenFutureCallback<T, F> {\n    /**\n     * Callback that is invoked when Future.then completes,\n     * and converts a value F to a Future<T>.\n     * @param from\n     * @return\n     * @throws Exception\n     */\n    Future<T> then(F from) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/TransformFuture.java",
    "content": "package com.jeffmony.async.future;\n\npublic abstract class TransformFuture<T, F> extends SimpleFuture<T> implements FutureCallback<F> {\n    public TransformFuture(F from) {\n        onCompleted(null, from);\n    }\n\n    public TransformFuture() {\n    }\n\n    @Override\n    public void onCompleted(Exception e, F result) {\n        if (isCancelled())\n            return;\n        if (e != null) {\n            error(e);\n            return;\n        }\n\n        try {\n            transform(result);\n        }\n        catch (Exception ex) {\n            error(ex);\n        }\n    }\n\n    protected void error(Exception e) {\n        setComplete(e);\n    }\n\n    protected abstract void transform(F result) throws Exception;\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/future/TypeConverter.java",
    "content": "package com.jeffmony.async.future;\n\npublic interface TypeConverter<T, F> {\n    Future<T> convert(F from, String fromMime) throws Exception;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpClient.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.annotation.SuppressLint;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.AsyncSSLException;\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.SimpleFuture;\nimport com.jeffmony.async.http.callback.HttpConnectCallback;\nimport com.jeffmony.async.http.callback.RequestCallback;\nimport com.jeffmony.async.parser.AsyncParser;\nimport com.jeffmony.async.parser.ByteBufferListParser;\nimport com.jeffmony.async.parser.JSONArrayParser;\nimport com.jeffmony.async.parser.JSONObjectParser;\nimport com.jeffmony.async.parser.StringParser;\nimport com.jeffmony.async.stream.OutputStreamDataCallback;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\nimport java.net.ProxySelector;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.TimeoutException;\n\npublic class AsyncHttpClient {\n    private static AsyncHttpClient mDefaultInstance;\n    public static AsyncHttpClient getDefaultInstance() {\n        if (mDefaultInstance == null)\n            mDefaultInstance = new AsyncHttpClient(AsyncServer.getDefault());\n\n        return mDefaultInstance;\n    }\n\n    final List<AsyncHttpClientMiddleware> mMiddleware = new CopyOnWriteArrayList<>();\n    public Collection<AsyncHttpClientMiddleware> getMiddleware() {\n        return mMiddleware;\n    }\n    public void insertMiddleware(AsyncHttpClientMiddleware middleware) {\n        mMiddleware.add(0, middleware);\n    }\n\n    AsyncSSLSocketMiddleware sslSocketMiddleware;\n    AsyncSocketMiddleware socketMiddleware;\n    HttpTransportMiddleware httpTransportMiddleware;\n    AsyncServer mServer;\n    public AsyncHttpClient(AsyncServer server) {\n        mServer = server;\n        insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this));\n        insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this));\n        insertMiddleware(httpTransportMiddleware = new HttpTransportMiddleware());\n        sslSocketMiddleware.addEngineConfigurator(new SSLEngineSNIConfigurator());\n    }\n\n    @SuppressLint(\"NewApi\")\n    private static void setupAndroidProxy(AsyncHttpRequest request) {\n        // using a explicit proxy?\n        if (request.proxyHost != null)\n            return;\n\n        List<Proxy> proxies;\n        try {\n            proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString()));\n        }\n        catch (Exception e) {\n            // uri parsing craps itself sometimes.\n            return;\n        }\n        if (proxies.isEmpty())\n            return;\n        Proxy proxy = proxies.get(0);\n        if (proxy.type() != Proxy.Type.HTTP)\n            return;\n        if (!(proxy.address() instanceof InetSocketAddress))\n            return;\n        InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();\n        String proxyHost;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {\n            proxyHost = proxyAddress.getHostString();\n        }\n        else {\n            InetAddress address = proxyAddress.getAddress();\n            if (address!=null)\n                proxyHost = address.getHostAddress();\n            else\n                proxyHost = proxyAddress.getHostName();\n        }\n        request.enableProxy(proxyHost, proxyAddress.getPort());\n    }\n\n    public AsyncSocketMiddleware getSocketMiddleware() {\n        return socketMiddleware;\n    }\n\n    public AsyncSSLSocketMiddleware getSSLSocketMiddleware() {\n        return sslSocketMiddleware;\n    }\n\n    public Future<AsyncHttpResponse> execute(final AsyncHttpRequest request, final HttpConnectCallback callback) {\n        FutureAsyncHttpResponse ret;\n        execute(request, 0, ret = new FutureAsyncHttpResponse(), callback);\n        return ret;\n    }\n\n    public Future<AsyncHttpResponse> execute(String uri, final HttpConnectCallback callback) {\n        return execute(new AsyncHttpGet(uri), callback);\n    }\n\n    private static final String LOGTAG = \"AsyncHttp\";\n    private class FutureAsyncHttpResponse extends SimpleFuture<AsyncHttpResponse> {\n        public AsyncSocket socket;\n        public Cancellable scheduled;\n        public Runnable timeoutRunnable;\n\n        @Override\n        public boolean cancel() {\n            if (!super.cancel())\n                return false;\n\n            if (socket != null) {\n                socket.setDataCallback(new DataCallback.NullDataCallback());\n                socket.close();\n            }\n\n            if (scheduled != null)\n                scheduled.cancel();\n\n            return true;\n        }\n    }\n\n    private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception ex, AsyncHttpResponseImpl response, AsyncHttpRequest request, final HttpConnectCallback callback) {\n        assert callback != null;\n        cancel.scheduled.cancel();\n        boolean complete;\n        if (ex != null) {\n            request.loge(\"Connection error\", ex);\n            complete = cancel.setComplete(ex);\n        }\n        else {\n            request.logd(\"Connection successful\");\n            complete = cancel.setComplete(response);\n        }\n        if (complete) {\n            callback.onConnectCompleted(ex, response);\n            assert ex != null || response.socket() == null || response.getDataCallback() != null || response.isPaused();\n            return;\n        }\n\n        if (response != null) {\n            // the request was cancelled, so close up shop, and eat any pending data\n            response.setDataCallback(new DataCallback.NullDataCallback());\n            response.close();\n        }\n    }\n\n    private void execute(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {\n        if (mServer.isAffinityThread()) {\n            executeAffinity(request, redirectCount, cancel, callback);\n        }\n        else {\n            mServer.post(new Runnable() {\n                @Override\n                public void run() {\n                    executeAffinity(request, redirectCount, cancel, callback);\n                }\n            });\n        }\n    }\n\n    private static long getTimeoutRemaining(AsyncHttpRequest request) {\n        // need a better way to calculate this.\n        // a timer of sorts that stops/resumes.\n        return request.getTimeout();\n    }\n\n    private static void copyHeader(AsyncHttpRequest from, AsyncHttpRequest to, String header) {\n        String value = from.getHeaders().get(header);\n        if (!TextUtils.isEmpty(value))\n            to.getHeaders().set(header, value);\n    }\n\n    private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {\n        assert mServer.isAffinityThread();\n        if (redirectCount > 15) {\n            reportConnectedCompleted(cancel, new RedirectLimitExceededException(\"too many redirects\"), null, request, callback);\n            return;\n        }\n        final Uri uri = request.getUri();\n        final AsyncHttpClientMiddleware.OnResponseCompleteData data = new AsyncHttpClientMiddleware.OnResponseCompleteData();\n        request.executionTime = System.currentTimeMillis();\n        data.request = request;\n\n        request.logd(\"Executing request.\");\n\n        for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n            middleware.onRequest(data);\n        }\n\n        // flow:\n        // 1) set a connect timeout\n        // 2) wait for connect\n        // 3) on connect, cancel timeout\n        // 4) wait for request to be sent fully\n        // 5) after request is sent, set a header timeout\n        // 6) wait for headers\n        // 7) on headers, cancel timeout\n        // 8) TODO: response can take as long as it wants to arrive?\n\n        if (request.getTimeout() > 0) {\n            // set connect timeout\n            cancel.timeoutRunnable = new Runnable() {\n                @Override\n                public void run() {\n                    // we've timed out, kill the connections\n                    if (data.socketCancellable != null) {\n                        data.socketCancellable.cancel();\n                        if (data.socket != null)\n                            data.socket.close();\n                    }\n                    reportConnectedCompleted(cancel, new TimeoutException(), null, request, callback);\n                }\n            };\n            cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request));\n        }\n\n        // 2) wait for a connect\n        data.connectCallback = new ConnectCallback() {\n            boolean reported;\n            @Override\n            public void onConnectCompleted(Exception ex, AsyncSocket socket) {\n                if (reported) {\n                    if (socket != null) {\n                        socket.setDataCallback(new DataCallback.NullDataCallback());\n                        socket.setEndCallback(new CompletedCallback.NullCompletedCallback());\n                        socket.close();\n                        throw new AssertionError(\"double connect callback\");\n                    }\n                }\n                reported = true;\n\n                request.logv(\"socket connected\");\n                if (cancel.isCancelled()) {\n                    if (socket != null)\n                        socket.close();\n                    return;\n                }\n\n                // 3) on connect, cancel timeout\n                if (cancel.timeoutRunnable != null)\n                    cancel.scheduled.cancel();\n\n                if (ex != null) {\n                    reportConnectedCompleted(cancel, ex, null, request, callback);\n                    return;\n                }\n\n                data.socket = socket;\n                cancel.socket = socket;\n\n                executeSocket(request, redirectCount, cancel, callback, data);\n            }\n        };\n\n        // set up the system default proxy and connect\n        setupAndroidProxy(request);\n\n        // set the implicit content type\n        if (request.getBody() != null) {\n            if (request.getHeaders().get(\"Content-Type\") == null)\n                request.getHeaders().set(\"Content-Type\", request.getBody().getContentType());\n        }\n\n        final Exception unsupportedURI;\n        for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n            Cancellable socketCancellable = middleware.getSocket(data);\n            if (socketCancellable != null) {\n                data.socketCancellable = socketCancellable;\n                cancel.setParent(socketCancellable);\n                return;\n            }\n        }\n        unsupportedURI = new IllegalArgumentException(\"invalid uri=\"+request.getUri()+\" middlewares=\"+mMiddleware);\n        reportConnectedCompleted(cancel, unsupportedURI, null, request, callback);\n    }\n\n    private void executeSocket(final AsyncHttpRequest request, final int redirectCount,\n                               final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback,\n                               final AsyncHttpClientMiddleware.OnResponseCompleteData data) {\n        // 4) wait for request to be sent fully\n        // and\n        // 6) wait for headers\n        final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) {\n            @Override\n            protected void onRequestCompleted(Exception ex) {\n                if (ex != null) {\n                    reportConnectedCompleted(cancel, ex, null, request, callback);\n                    return;\n                }\n\n                request.logv(\"request completed\");\n                if (cancel.isCancelled())\n                    return;\n                // 5) after request is sent, set a header timeout\n                if (cancel.timeoutRunnable != null && mHeaders == null) {\n                    cancel.scheduled.cancel();\n                    cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request));\n                }\n\n                for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n                    middleware.onRequestSent(data);\n                }\n            }\n\n            @Override\n            public void setDataEmitter(DataEmitter emitter) {\n                data.bodyEmitter = emitter;\n                for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n                    middleware.onBodyDecoder(data);\n                }\n\n                super.setDataEmitter(data.bodyEmitter);\n\n                for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n                    AsyncHttpRequest newReq = middleware.onResponseReady(data);\n                    if (newReq != null) {\n                        newReq.executionTime = request.executionTime;\n                        newReq.logLevel = request.logLevel;\n                        newReq.LOGTAG = request.LOGTAG;\n                        newReq.proxyHost = request.proxyHost;\n                        newReq.proxyPort = request.proxyPort;\n                        setupAndroidProxy(newReq);\n\n                        request.logi(\"Response intercepted by middleware\");\n                        newReq.logi(\"Request initiated by middleware intercept by middleware\");\n                        // post to allow reuse of socket.\n                        mServer.post(() -> execute(newReq, redirectCount, cancel, callback));\n                        setDataCallback(new NullDataCallback());\n                        return;\n                    }\n                }\n\n                Headers headers = mHeaders;\n                int responseCode = code();\n                if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) {\n                    String location = headers.get(\"Location\");\n                    Uri redirect;\n                    try {\n                        redirect = Uri.parse(location);\n                        if (redirect.getScheme() == null) {\n                            redirect = Uri.parse(new URL(new URL(request.getUri().toString()), location).toString());\n                        }\n                    }\n                    catch (Exception e) {\n                        reportConnectedCompleted(cancel, e, this, request, callback);\n                        return;\n                    }\n                    final String method = request.getMethod().equals(AsyncHttpHead.METHOD) ? AsyncHttpHead.METHOD : AsyncHttpGet.METHOD;\n                    AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, method);\n                    newReq.executionTime = request.executionTime;\n                    newReq.logLevel = request.logLevel;\n                    newReq.LOGTAG = request.LOGTAG;\n                    newReq.proxyHost = request.proxyHost;\n                    newReq.proxyPort = request.proxyPort;\n                    setupAndroidProxy(newReq);\n                    copyHeader(request, newReq, \"User-Agent\");\n                    copyHeader(request, newReq, \"Range\");\n                    request.logi(\"Redirecting\");\n                    newReq.logi(\"Redirected\");\n                    mServer.post(() -> execute(newReq, redirectCount + 1, cancel, callback));\n\n                    setDataCallback(new NullDataCallback());\n                    return;\n                }\n\n                request.logv(\"Final (post cache response) headers:\\n\" + toString());\n\n                // at this point the headers are done being modified\n                reportConnectedCompleted(cancel, null, this, request, callback);\n            }\n\n            protected void onHeadersReceived() {\n                super.onHeadersReceived();\n                if (cancel.isCancelled())\n                    return;\n\n                // 7) on headers, cancel timeout\n                if (cancel.timeoutRunnable != null)\n                    cancel.scheduled.cancel();\n\n                // allow the middleware to massage the headers before the body is decoded\n                request.logv(\"Received headers:\\n\" + toString());\n\n                for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n                    middleware.onHeadersReceived(data);\n                }\n\n                // drop through, and setDataEmitter will be called for the body decoder.\n                // headers will be further massaged in there.\n            }\n\n            @Override\n            protected void report(Exception ex) {\n                if (ex != null)\n                    request.loge(\"exception during response\", ex);\n                if (cancel.isCancelled())\n                    return;\n                if (ex instanceof AsyncSSLException) {\n                    request.loge(\"SSL Exception\", ex);\n                    AsyncSSLException ase = (AsyncSSLException)ex;\n                    request.onHandshakeException(ase);\n                    if (ase.getIgnore())\n                        return;\n                }\n                final AsyncSocket socket = socket();\n                if (socket == null)\n                    return;\n                super.report(ex);\n                if (!socket.isOpen() || ex != null) {\n                    if (headers() == null && ex != null)\n                        reportConnectedCompleted(cancel, ex, null, request, callback);\n                }\n\n                data.exception = ex;\n                for (AsyncHttpClientMiddleware middleware: mMiddleware) {\n                    middleware.onResponseComplete(data);\n                }\n            }\n\n            @Override\n            public AsyncSocket detachSocket() {\n                request.logd(\"Detaching socket\");\n                AsyncSocket socket = socket();\n                if (socket == null)\n                    return null;\n                socket.setWriteableCallback(null);\n                socket.setClosedCallback(null);\n                socket.setEndCallback(null);\n                socket.setDataCallback(null);\n                setSocket(null);\n                return socket;\n            }\n        };\n\n        data.sendHeadersCallback = new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (ex != null)\n                    ret.report(ex);\n                else\n                    ret.onHeadersSent();\n            }\n        };\n        data.receiveHeadersCallback = new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (ex != null)\n                    ret.report(ex);\n                else\n                    ret.onHeadersReceived();\n            }\n        };\n        data.response = ret;\n        ret.setSocket(data.socket);\n\n        for (AsyncHttpClientMiddleware middleware : mMiddleware) {\n            if (middleware.exchangeHeaders(data))\n                break;\n        }\n    }\n\n    public static abstract class RequestCallbackBase<T> implements RequestCallback<T> {\n        @Override\n        public void onProgress(AsyncHttpResponse response, long downloaded, long total) {\n        }\n        @Override\n        public void onConnect(AsyncHttpResponse response) {\n        }\n    }\n\n    public static abstract class DownloadCallback extends RequestCallbackBase<ByteBufferList> {\n    }\n\n    public static abstract class StringCallback extends RequestCallbackBase<String> {\n    }\n\n    public static abstract class JSONObjectCallback extends RequestCallbackBase<JSONObject> {\n    }\n    \n    public static abstract class JSONArrayCallback extends RequestCallbackBase<JSONArray> {\n    }\n\n    public static abstract class FileCallback extends RequestCallbackBase<File> {\n    }\n\n    public Future<ByteBufferList> executeByteBufferList(AsyncHttpRequest request, DownloadCallback callback) {\n        return execute(request, new ByteBufferListParser(), callback);\n    }\n\n    public Future<String> executeString(AsyncHttpRequest req, final StringCallback callback) {\n        return execute(req, new StringParser(), callback);\n    }\n\n    public Future<JSONObject> executeJSONObject(AsyncHttpRequest req, final JSONObjectCallback callback) {\n        return execute(req, new JSONObjectParser(), callback);\n    }\n\n    public Future<JSONArray> executeJSONArray(AsyncHttpRequest req, final JSONArrayCallback callback) {\n        return execute(req, new JSONArrayParser(), callback);\n    }\n\n    private <T> void invokeWithAffinity(final RequestCallback<T> callback, SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {\n        boolean complete;\n        if (e != null)\n            complete = future.setComplete(e);\n        else\n            complete = future.setComplete(result);\n        if (!complete)\n            return;\n        if (callback != null)\n            callback.onCompleted(e, response, result);\n    }\n\n    private <T> void invoke(final RequestCallback<T> callback, final SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {\n        Runnable runnable = new Runnable() {\n            @Override\n            public void run() {\n                invokeWithAffinity(callback, future, response, e, result);\n            }\n        };\n        mServer.post(runnable);\n    }\n\n    private void invokeProgress(final RequestCallback callback, final AsyncHttpResponse response, final long downloaded, final long total) {\n        if (callback != null)\n            callback.onProgress(response, downloaded, total);\n    }\n\n    private void invokeConnect(final RequestCallback callback, final AsyncHttpResponse response) {\n        if (callback != null)\n            callback.onConnect(response);\n    }\n\n    public Future<File> executeFile(AsyncHttpRequest req, final String filename, final FileCallback callback) {\n        final File file = new File(filename);\n        file.getParentFile().mkdirs();\n        final OutputStream fout;\n        try {\n            fout = new BufferedOutputStream(new FileOutputStream(file), 8192);\n        }\n        catch (FileNotFoundException e) {\n            SimpleFuture<File> ret = new SimpleFuture<File>();\n            ret.setComplete(e);\n            return ret;\n        }\n        final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();\n        final SimpleFuture<File> ret = new SimpleFuture<File>() {\n            @Override\n            public void cancelCleanup() {\n                try {\n                    cancel.get().setDataCallback(new DataCallback.NullDataCallback());\n                    cancel.get().close();\n                }\n                catch (Exception e) {\n                }\n                try {\n                    fout.close();\n                }\n                catch (Exception e) {\n                }\n                file.delete();\n            }\n        };\n        ret.setParent(cancel);\n        execute(req, 0, cancel, new HttpConnectCallback() {\n            long mDownloaded = 0;\n\n            @Override\n            public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {\n                if (ex != null) {\n                    try {\n                        fout.close();\n                    }\n                    catch (IOException e) {\n                    }\n                    file.delete();\n                    invoke(callback, ret, response, ex, null);\n                    return;\n                }\n                invokeConnect(callback, response);\n\n                final long contentLength = HttpUtil.contentLength(response.headers());\n\n                response.setDataCallback(new OutputStreamDataCallback(fout) {\n                    @Override\n                    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                        mDownloaded += bb.remaining();\n                        super.onDataAvailable(emitter, bb);\n                        invokeProgress(callback, response, mDownloaded, contentLength);\n                    }\n                });\n                response.setEndCallback(new CompletedCallback() {\n                    @Override\n                    public void onCompleted(Exception ex) {\n                        try {\n                            fout.close();\n                        }\n                        catch (IOException e) {\n                            ex = e;\n                        }\n                        if (ex != null) {\n                            file.delete();\n                            invoke(callback, ret, response, ex, null);\n                        }\n                        else {\n                            invoke(callback, ret, response, null, file);\n                        }\n                    }\n                });\n            }\n        });\n        return ret;\n    }\n\n    public <T> SimpleFuture<T> execute(AsyncHttpRequest req, final AsyncParser<T> parser, final RequestCallback<T> callback) {\n        final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();\n        final SimpleFuture<T> ret = new SimpleFuture<T>();\n        execute(req, 0, cancel, (ex, response) -> {\n            if (ex != null) {\n                invoke(callback, ret, response, ex, null);\n                return;\n            }\n            invokeConnect(callback, response);\n\n            Future<T> parsed = parser.parse(response);\n            parsed.setCallback((e, result) -> invoke(callback, ret, response, e, result));\n\n            // reparent to the new parser future\n            ret.setParent(parsed);\n        });\n        ret.setParent(cancel);\n        return ret;\n    }\n\n    public interface WebSocketConnectCallback {\n        void onCompleted(Exception ex, WebSocket webSocket);\n    }\n\n    public Future<WebSocket> websocket(final AsyncHttpRequest req, String protocol, final WebSocketConnectCallback callback) {\n        return websocket(req, protocol != null ? new String[] { protocol } : null, callback);\n    }\n\n    public Future<WebSocket> websocket(final AsyncHttpRequest req, String[] protocols, final WebSocketConnectCallback callback) {\n        WebSocketImpl.addWebSocketUpgradeHeaders(req, protocols);\n        final SimpleFuture<WebSocket> ret = new SimpleFuture<>();\n        Cancellable connect = execute(req, (ex, response) -> {\n            if (ex != null) {\n                if (ret.setComplete(ex)) {\n                    if (callback != null)\n                        callback.onCompleted(ex, null);\n                }\n                return;\n            }\n            WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders(), response);\n            if (ws == null) {\n                ex = new WebSocketHandshakeException(\"Unable to complete websocket handshake\");\n                response.close();\n                if (!ret.setComplete(ex))\n                    return;\n            }\n            else {\n                if (!ret.setComplete(ws))\n                    return;\n            }\n            if (callback != null)\n                callback.onCompleted(ex, ws);\n        });\n\n        ret.setParent(connect);\n        return ret;\n    }\n\n    public Future<WebSocket> websocket(String uri, String protocol, final WebSocketConnectCallback callback) {\n//        assert callback != null;\n        final AsyncHttpGet get = new AsyncHttpGet(uri.replace(\"ws://\", \"http://\").replace(\"wss://\", \"https://\"));\n        return websocket(get, protocol, callback);\n    }\n\n    public Future<WebSocket> websocket(String uri, String[] protocols, final WebSocketConnectCallback callback) {\n//        assert callback != null;\n        final AsyncHttpGet get = new AsyncHttpGet(uri.replace(\"ws://\", \"http://\").replace(\"wss://\", \"https://\"));\n        return websocket(get, protocols, callback);\n    }\n\n    public AsyncServer getServer() {\n        return mServer;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpClientMiddleware.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.util.UntypedHashtable;\n\n/**\n * AsyncHttpClientMiddleware is used by AsyncHttpClient to\n * inspect, manipulate, and handle http requests.\n */\npublic interface AsyncHttpClientMiddleware {\n    interface ResponseHead  {\n        AsyncSocket socket();\n        String protocol();\n        String message();\n        int code();\n        ResponseHead protocol(String protocol);\n        ResponseHead message(String message);\n        ResponseHead code(int code);\n        Headers headers();\n        ResponseHead headers(Headers headers);\n        DataSink sink();\n        ResponseHead sink(DataSink sink);\n        DataEmitter emitter();\n        ResponseHead emitter(DataEmitter emitter);\n    }\n\n    class OnRequestData {\n        public UntypedHashtable state = new UntypedHashtable();\n        public AsyncHttpRequest request;\n    }\n\n    class GetSocketData extends OnRequestData {\n        public ConnectCallback connectCallback;\n        public Cancellable socketCancellable;\n        public String protocol;\n    }\n\n    class OnExchangeHeaderData extends GetSocketData {\n        public AsyncSocket socket;\n        public ResponseHead response;\n        public CompletedCallback sendHeadersCallback;\n        public CompletedCallback receiveHeadersCallback;\n    }\n\n    class OnRequestSentData extends OnExchangeHeaderData {\n    }\n\n    class OnHeadersReceivedData extends OnRequestSentData {\n    }\n\n    class OnBodyDecoderData extends OnHeadersReceivedData {\n        public DataEmitter bodyEmitter;\n    }\n\n    class OnResponseReadyData extends OnBodyDecoderData {\n    }\n\n    class OnResponseCompleteData extends OnResponseReadyData {\n        public Exception exception;\n    }\n\n    /**\n     * Called immediately upon request execution\n     * @param data\n     */\n    void onRequest(OnRequestData data);\n\n    /**\n     * Called to retrieve the socket that will fulfill this request\n     * @param data\n     * @return\n     */\n    Cancellable getSocket(GetSocketData data);\n\n    /**\n     * Called before when the headers are sent and received via the socket.\n     * Implementers return true to denote they will manage header exchange.\n     * @param data\n     * @return\n     */\n    boolean exchangeHeaders(OnExchangeHeaderData data);\n\n    /**\n     * Called once the headers and any optional request body has\n     * been sent\n     * @param data\n     */\n    void onRequestSent(OnRequestSentData data);\n\n    /**\n     * Called once the headers have been received via the socket\n     * @param data\n     */\n    void onHeadersReceived(OnHeadersReceivedData data);\n\n    /**\n     * Called before the body is decoded\n     * @param data\n     */\n    void onBodyDecoder(OnBodyDecoderData data);\n\n    /**\n     * Called before the response is returned to the client. Return a new AsyncHttpRequest\n     * to end the current request and start a new one. Can be used to implement redirect strategies\n     * or multileg authentication, such as digest.\n     * @param data\n     * @return\n     */\n    AsyncHttpRequest onResponseReady(OnResponseReadyData data);\n\n    /**\n     * Called once the request is complete and response has been received,\n     * or if an error occurred\n     * @param data\n     */\n    void onResponseComplete(OnResponseCompleteData data);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpDelete.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\npublic class AsyncHttpDelete extends AsyncHttpRequest {\n    public static final String METHOD = \"DELETE\";\n\n    public AsyncHttpDelete(String uri) {\n        this(Uri.parse(uri));\n    }\n\n    public AsyncHttpDelete(Uri uri) {\n        super(uri, METHOD);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpGet.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\npublic class AsyncHttpGet extends AsyncHttpRequest {\n    public static final String METHOD = \"GET\";\n    \n    public AsyncHttpGet(String uri) {\n        super(Uri.parse(uri), METHOD);\n    }\n\n    public AsyncHttpGet(Uri uri) {\n        super(uri, METHOD);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpHead.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\n/**\n * Created by koush on 8/25/13.\n */\npublic class AsyncHttpHead extends AsyncHttpRequest {\n    public AsyncHttpHead(Uri uri) {\n        super(uri, METHOD);\n    }\n\n    @Override\n    public boolean hasBody() {\n        return false;\n    }\n\n    public static final String METHOD = \"HEAD\";\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpPost.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\npublic class AsyncHttpPost extends AsyncHttpRequest {\n    public static final String METHOD = \"POST\";\n    \n    public AsyncHttpPost(String uri) {\n        this(Uri.parse(uri));\n    }\n\n    public AsyncHttpPost(Uri uri) {\n        super(uri, METHOD);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpPut.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\npublic class AsyncHttpPut extends AsyncHttpRequest {\n    public static final String METHOD = \"PUT\";\n    \n    public AsyncHttpPut(String uri) {\n        this(Uri.parse(uri));\n    }\n\n    public AsyncHttpPut(Uri uri) {\n        super(uri, METHOD);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpRequest.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\nimport android.util.Log;\n\nimport com.jeffmony.async.AsyncSSLException;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\nimport java.util.Locale;\n\npublic class AsyncHttpRequest {\n    public RequestLine getRequestLine() {\n        return new RequestLine() {\n            @Override\n            public String getUri() {\n                return AsyncHttpRequest.this.getUri().toString();\n            }\n            \n            @Override\n            public ProtocolVersion getProtocolVersion() {\n                return new ProtocolVersion(\"HTTP\", 1, 1);\n            }\n            \n            @Override\n            public String getMethod() {\n                return mMethod;\n            }\n\n            @Override\n            public String toString() {\n                if (proxyHost != null)\n                    return String.format(Locale.ENGLISH, \"%s %s %s\", mMethod, AsyncHttpRequest.this.getUri(), requestLineProtocol);\n                String path = getPath();\n                if (path == null || path.length() == 0)\n                    path = \"/\";\n                String query = AsyncHttpRequest.this.getUri().getEncodedQuery();\n                if (query != null && query.length() != 0) {\n                    path += \"?\" + query;\n                }\n                return String.format(Locale.ENGLISH, \"%s %s %s\", mMethod, path, requestLineProtocol);\n            }\n        };\n    }\n\n    public boolean hasBody() {\n        return true;\n    }\n\n    public String getPath() {\n        return AsyncHttpRequest.this.getUri().getEncodedPath();\n    }\n\n    protected static String getDefaultUserAgent() {\n        String agent = System.getProperty(\"http.agent\");\n        return agent != null ? agent : (\"Java\" + System.getProperty(\"java.version\"));\n    }\n\n    private String requestLineProtocol = \"HTTP/1.1\";\n    private String mMethod;\n    public String getMethod() {\n       return mMethod; \n    }\n\n    public void setRequestLineProtocol(String scheme) {\n        this.requestLineProtocol = scheme;\n    }\n\n    public String getRequestLineProtocol() {\n        return requestLineProtocol;\n    }\n\n    public AsyncHttpRequest setMethod(String method) {\n        if (getClass() != AsyncHttpRequest.class)\n            throw new UnsupportedOperationException(\"can't change method on a subclass of AsyncHttpRequest\");\n        mMethod = method;\n        return this;\n    }\n\n    public AsyncHttpRequest(Uri uri, String method) {\n        this(uri, method, null);\n    }\n\n    public static void setDefaultHeaders(Headers ret, Uri uri) {\n        if (uri != null) {\n            String host = uri.getHost();\n            if (uri.getPort() != -1)\n                host = host + \":\" + uri.getPort();\n            if (host != null)\n                ret.set(\"Host\", host);\n        }\n        ret.set(\"User-Agent\", getDefaultUserAgent());\n        ret.set(\"Accept-Encoding\", \"gzip, deflate\");\n        ret.set(\"Connection\", \"keep-alive\");\n        ret.set(\"Accept\", HEADER_ACCEPT_ALL);\n    }\n\n    public static final String HEADER_ACCEPT_ALL = \"*/*\";\n\n    public AsyncHttpRequest(Uri uri, String method, Headers headers) {\n        assert uri != null;\n        mMethod = method;\n        this.uri = uri;\n        if (headers == null)\n            mRawHeaders = new Headers();\n        else\n            mRawHeaders = headers;\n        if (headers == null)\n            setDefaultHeaders(mRawHeaders, uri);\n    }\n\n    Uri uri;\n    public Uri getUri() {\n        return uri;\n    }\n    \n    private Headers mRawHeaders = new Headers();\n\n    public Headers getHeaders() {\n        return mRawHeaders;\n    }\n\n    private boolean mFollowRedirect = true;\n    public boolean getFollowRedirect() {\n        return mFollowRedirect;\n    }\n    public AsyncHttpRequest setFollowRedirect(boolean follow) {\n        mFollowRedirect = follow;\n        return this;\n    }\n    \n    private AsyncHttpRequestBody mBody;\n    public void setBody(AsyncHttpRequestBody body) {\n        mBody = body;\n    }\n    \n    public AsyncHttpRequestBody getBody() {\n        return mBody;\n    }\n    \n    public void onHandshakeException(AsyncSSLException e) {\n    }\n\n    public static final int DEFAULT_TIMEOUT = 30000;\n    int mTimeout = DEFAULT_TIMEOUT;\n    public int getTimeout() {\n        return mTimeout;\n    }\n    \n    public AsyncHttpRequest setTimeout(int timeout) {\n        mTimeout = timeout;\n        return this;\n    }\n\n    public AsyncHttpRequest setHeader(String name, String value) {\n        getHeaders().set(name, value);\n        return this;\n    }\n\n    public AsyncHttpRequest addHeader(String name, String value) {\n        getHeaders().add(name, value);\n        return this;\n    }\n\n    String proxyHost;\n    int proxyPort = -1;\n    public void enableProxy(String host, int port) {\n        proxyHost = host;\n        proxyPort = port;\n    }\n\n    public void disableProxy() {\n        proxyHost = null;\n        proxyPort = -1;\n    }\n\n    public String getProxyHost() {\n        return proxyHost;\n    }\n\n    public int getProxyPort() {\n        return proxyPort;\n    }\n\n    @Override\n    public String toString() {\n        if (mRawHeaders == null)\n            return super.toString();\n        return mRawHeaders.toPrefixString(uri.toString());\n    }\n\n    public void setLogging(String tag, int level) {\n        LOGTAG = tag;\n        logLevel = level;\n    }\n    // request level logging\n    String LOGTAG;\n    int logLevel;\n    public int getLogLevel() {\n        return logLevel;\n    }\n    public String getLogTag() {\n        return LOGTAG;\n    }\n    long executionTime;\n    private String getLogMessage(String message) {\n        long elapsed;\n        if (executionTime != 0)\n            elapsed = System.currentTimeMillis() - executionTime;\n        else\n            elapsed = 0;\n        return String.format(Locale.ENGLISH, \"(%d ms) %s: %s\", elapsed, getUri(), message);\n    }\n    public void logi(String message) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.INFO)\n            return;\n        Log.i(LOGTAG, getLogMessage(message));\n    }\n    public void logv(String message) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.VERBOSE)\n            return;\n        Log.v(LOGTAG, getLogMessage(message));\n    }\n    public void logw(String message) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.WARN)\n            return;\n        Log.w(LOGTAG, getLogMessage(message));\n    }\n    public void logd(String message) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.DEBUG)\n            return;\n        Log.d(LOGTAG, getLogMessage(message));\n    }\n    public void logd(String message, Exception e) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.DEBUG)\n            return;\n        Log.d(LOGTAG, getLogMessage(message));\n        Log.d(LOGTAG, e.getMessage(), e);\n    }\n    public void loge(String message) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.ERROR)\n            return;\n        Log.e(LOGTAG, getLogMessage(message));\n    }\n    public void loge(String message, Exception e) {\n        if (LOGTAG == null)\n            return;\n        if (logLevel > Log.ERROR)\n            return;\n        Log.e(LOGTAG, getLogMessage(message));\n        Log.e(LOGTAG, e.getMessage(), e);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpResponse.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.DataEmitter;\n\npublic interface AsyncHttpResponse extends DataEmitter {\n    String protocol();\n    String message();\n    int code();\n    Headers headers();\n    AsyncSocket detachSocket();\n    AsyncHttpRequest getRequest();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncHttpResponseImpl.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.FilteredDataEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\nimport java.nio.charset.Charset;\n\nabstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements DataEmitter, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead {\n    public AsyncSocket socket() {\n        return mSocket;\n    }\n\n    @Override\n    public AsyncHttpRequest getRequest() {\n        return mRequest;\n    }\n\n    void setSocket(AsyncSocket exchange) {\n        mSocket = exchange;\n        if (mSocket == null)\n            return;\n\n        mSocket.setEndCallback(mReporter);\n    }\n\n    protected void onHeadersSent() {\n        AsyncHttpRequestBody requestBody = mRequest.getBody();\n        if (requestBody != null) {\n            requestBody.write(mRequest, mSink, new CompletedCallback() {\n                @Override\n                public void onCompleted(Exception ex) {\n                    onRequestCompleted(ex);\n                }\n            });\n        } else {\n            onRequestCompleted(null);\n        }\n    }\n\n    protected void onRequestCompleted(Exception ex) {\n    }\n    \n    private CompletedCallback mReporter = new CompletedCallback() {\n        @Override\n        public void onCompleted(Exception error) {\n            if (headers() == null) {\n                report(new ConnectionClosedException(\"connection closed before headers received.\", error));\n            }\n            else if (error != null && !mCompleted) {\n                report(new ConnectionClosedException(\"connection closed before response completed.\", error));\n            }\n            else {\n                report(error);\n            }\n        }\n    };\n\n    protected void onHeadersReceived() {\n    }\n\n\n    @Override\n    public DataEmitter emitter() {\n        return getDataEmitter();\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead emitter(DataEmitter emitter) {\n        setDataEmitter(emitter);\n        return this;\n    }\n\n    private void terminate() {\n        // DISCONNECT. EVERYTHING.\n        // should not get any data after this point...\n        // if so, eat it and disconnect.\n        mSocket.setDataCallback(new NullDataCallback() {\n            @Override\n            public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                super.onDataAvailable(emitter, bb);\n                mSocket.close();\n            }\n        });\n    }\n\n    @Override\n    protected void report(Exception e) {\n        super.report(e);\n\n        terminate();\n        mSocket.setWriteableCallback(null);\n        mSocket.setClosedCallback(null);\n        mSocket.setEndCallback(null);\n        mCompleted = true;\n    }\n\n    @Override\n    public void close() {\n        super.close();\n        terminate();\n    }\n\n    private AsyncHttpRequest mRequest;\n    private AsyncSocket mSocket;\n    protected Headers mHeaders;\n    public AsyncHttpResponseImpl(AsyncHttpRequest request) {\n        mRequest = request;\n    }\n\n    boolean mCompleted = false;\n\n    @Override\n    public Headers headers() {\n        return mHeaders;\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead headers(Headers headers) {\n        mHeaders = headers;\n        return this;\n    }\n\n    int code;\n    @Override\n    public int code() {\n        return code;\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead code(int code) {\n        this.code = code;\n        return this;\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead protocol(String protocol) {\n        this.protocol = protocol;\n        return this;\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead message(String message) {\n        this.message = message;\n        return this;\n    }\n\n    String protocol;\n    @Override\n    public String protocol() {\n        return protocol;\n    }\n\n    String message;\n    @Override\n    public String message() {\n        return message;\n    }\n\n    @Override\n    public String toString() {\n        if (mHeaders == null)\n            return super.toString();\n        return mHeaders.toPrefixString(protocol + \" \" + code + \" \" + message);\n    }\n\n    private boolean mFirstWrite = true;\n    private void assertContent() {\n        if (!mFirstWrite)\n            return;\n        mFirstWrite = false;\n        assert null != mRequest.getHeaders().get(\"Content-Type\");\n        assert mRequest.getHeaders().get(\"Transfer-Encoding\") != null || HttpUtil.contentLength(mRequest.getHeaders()) != -1;\n    }\n\n    DataSink mSink;\n\n    @Override\n    public DataSink sink() {\n        return mSink;\n    }\n\n    @Override\n    public AsyncHttpClientMiddleware.ResponseHead sink(DataSink sink) {\n        mSink = sink;\n        return this;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mSocket.getServer();\n    }\n\n    @Override\n    public String charset() {\n        Multimap mm = Multimap.parseSemicolonDelimited(headers().get(\"Content-Type\"));\n        String cs;\n        if (mm != null && null != (cs = mm.getString(\"charset\")) && Charset.isSupported(cs)) {\n            return cs;\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncSSLEngineConfigurator.java",
    "content": "package com.jeffmony.async.http;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLEngine;\n\npublic interface AsyncSSLEngineConfigurator {\n    SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort);\n    void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncSSLSocketMiddleware.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.AsyncSSLSocket;\nimport com.jeffmony.async.AsyncSSLSocketWrapper;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.LineEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLEngine;\nimport javax.net.ssl.TrustManager;\n\npublic class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware {\n    public AsyncSSLSocketMiddleware(AsyncHttpClient client) {\n        super(client, \"https\", 443);\n    }\n\n    protected SSLContext sslContext;\n\n    public void setSSLContext(SSLContext sslContext) {\n        this.sslContext = sslContext;\n    }\n\n    public SSLContext getSSLContext() {\n        return sslContext != null ? sslContext : AsyncSSLSocketWrapper.getDefaultSSLContext();\n    }\n\n    protected TrustManager[] trustManagers;\n\n    public void setTrustManagers(TrustManager[] trustManagers) {\n        this.trustManagers = trustManagers;\n    }\n\n    protected HostnameVerifier hostnameVerifier;\n\n    public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {\n        this.hostnameVerifier = hostnameVerifier;\n    }\n\n    protected List<AsyncSSLEngineConfigurator> engineConfigurators = new ArrayList<AsyncSSLEngineConfigurator>();\n\n    public void addEngineConfigurator(AsyncSSLEngineConfigurator engineConfigurator) {\n        engineConfigurators.add(engineConfigurator);\n    }\n\n    public void clearEngineConfigurators() {\n        engineConfigurators.clear();\n    }\n\n    protected SSLEngine createConfiguredSSLEngine(GetSocketData data, String host, int port) {\n        SSLContext sslContext = getSSLContext();\n        SSLEngine sslEngine = null;\n\n        for (AsyncSSLEngineConfigurator configurator : engineConfigurators) {\n            sslEngine = configurator.createEngine(sslContext, host, port);\n            if (sslEngine != null)\n                break;\n        }\n\n        for (AsyncSSLEngineConfigurator configurator : engineConfigurators) {\n            configurator.configureEngine(sslEngine, data, host, port);\n        }\n\n        return sslEngine;\n    }\n\n    protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final GetSocketData data, final ConnectCallback callback) {\n        return new AsyncSSLSocketWrapper.HandshakeCallback() {\n            @Override\n            public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {\n                callback.onConnectCompleted(e, socket);\n            }\n        };\n    }\n\n    protected void tryHandshake(AsyncSocket socket, GetSocketData data, final Uri uri, final int port, final ConnectCallback callback) {\n        AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port,\n        createConfiguredSSLEngine(data, uri.getHost(), port),\n        trustManagers, hostnameVerifier, true,\n        createHandshakeCallback(data, callback));\n    }\n\n    @Override\n    protected ConnectCallback wrapCallback(final GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) {\n        return new ConnectCallback() {\n            @Override\n            public void onConnectCompleted(Exception ex, final AsyncSocket socket) {\n                if (ex != null) {\n                    callback.onConnectCompleted(ex, socket);\n                    return;\n                }\n\n                if (!proxied) {\n                    tryHandshake(socket, data, uri, port, callback);\n                    return;\n                }\n\n                // this SSL connection is proxied, must issue a CONNECT request to the proxy server\n                // http://stackoverflow.com/a/6594880/704837\n                // some proxies also require 'Host' header, it should be safe to provide it every time\n                String connect = String.format(Locale.ENGLISH, \"CONNECT %s:%s HTTP/1.1\\r\\nHost: %s\\r\\n\\r\\n\", uri.getHost(), port, uri.getHost());\n                data.request.logv(\"Proxying: \" + connect);\n                Util.writeAll(socket, connect.getBytes(), new CompletedCallback() {\n                    @Override\n                    public void onCompleted(Exception ex) {\n                        if (ex != null) {\n                            callback.onConnectCompleted(ex, socket);\n                            return;\n                        }\n\n                        LineEmitter liner = new LineEmitter();\n                        liner.setLineCallback(new LineEmitter.StringCallback() {\n                            String statusLine;\n                            @Override\n                            public void onStringAvailable(String s) {\n                                data.request.logv(s);\n                                if (statusLine == null) {\n                                    statusLine = s.trim();\n                                    if (!statusLine.matches(\"HTTP/1.\\\\d 2\\\\d\\\\d .*\")) { // connect response is allowed to have any 2xx status code\n                                        socket.setDataCallback(null);\n                                        socket.setEndCallback(null);\n                                        callback.onConnectCompleted(new IOException(\"non 2xx status line: \" + statusLine), socket);\n                                    }\n                                }\n                                else if (TextUtils.isEmpty(s.trim())) { // skip all headers, complete handshake once empty line is received\n                                    socket.setDataCallback(null);\n                                    socket.setEndCallback(null);\n                                    tryHandshake(socket, data, uri, port, callback);\n                                }\n                            }\n                        });\n\n                        socket.setDataCallback(liner);\n\n                        socket.setEndCallback(new CompletedCallback() {\n                            @Override\n                            public void onCompleted(Exception ex) {\n                                if (!socket.isOpen() && ex == null)\n                                    ex = new IOException(\"socket closed before proxy connect response\");\n                                callback.onConnectCompleted(ex, socket);\n                            }\n                        });\n                    }\n                });\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/AsyncSocketMiddleware.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ConnectCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.Futures;\nimport com.jeffmony.async.future.SimpleCancellable;\nimport com.jeffmony.async.future.SimpleFuture;\nimport com.jeffmony.async.util.ArrayDeque;\n\nimport java.net.InetSocketAddress;\nimport java.util.Hashtable;\nimport java.util.Locale;\n\npublic class AsyncSocketMiddleware extends SimpleMiddleware {\n    String scheme;\n    int port;\n    // 5 min idle timeout\n    int idleTimeoutMs = 300 * 1000;\n\n    public AsyncSocketMiddleware(AsyncHttpClient client, String scheme, int port) {\n        mClient = client;\n        this.scheme = scheme;\n        this.port = port;\n    }\n\n    public void setIdleTimeoutMs(int idleTimeoutMs) {\n        this.idleTimeoutMs = idleTimeoutMs;\n    }\n    \n    public int getSchemePort(Uri uri) {\n        if (uri.getScheme() == null || !uri.getScheme().equals(scheme))\n            return -1;\n        if (uri.getPort() == -1) {\n            return port;\n        }\n        else {\n            return uri.getPort();\n        }\n    }\n\n    public AsyncSocketMiddleware(AsyncHttpClient client) {\n        this(client, \"http\", 80);\n    }\n\n    protected AsyncHttpClient mClient;\n\n    protected ConnectCallback wrapCallback(GetSocketData data, Uri uri, int port, boolean proxied, ConnectCallback callback) {\n        return callback;\n    }\n\n    boolean connectAllAddresses;\n    public boolean getConnectAllAddresses() {\n        return connectAllAddresses;\n    }\n\n    public void setConnectAllAddresses(boolean connectAllAddresses) {\n        this.connectAllAddresses = connectAllAddresses;\n    }\n\n    String proxyHost;\n    int proxyPort;\n    InetSocketAddress proxyAddress;\n\n    public void disableProxy() {\n        proxyPort = -1;\n        proxyHost = null;\n        proxyAddress = null;\n    }\n\n    public void enableProxy(String host, int port) {\n        proxyHost = host;\n        proxyPort = port;\n        proxyAddress = null;\n    }\n\n    String computeLookup(Uri uri, int port, String proxyHost, int proxyPort) {\n        String proxy;\n        if (proxyHost != null)\n            proxy = proxyHost + \":\" + proxyPort;\n        else\n            proxy = \"\";\n\n        if (proxyHost != null)\n            proxy = proxyHost + \":\" + proxyPort;\n\n        return uri.getScheme() + \"//\" + uri.getHost() + \":\" + port + \"?proxy=\" + proxy;\n    }\n\n    class IdleSocketHolder {\n        public IdleSocketHolder(AsyncSocket socket) {\n            this.socket = socket;\n        }\n        AsyncSocket socket;\n        long idleTime = System.currentTimeMillis();\n    }\n\n    static class ConnectionInfo {\n        int openCount;\n        ArrayDeque<GetSocketData> queue = new ArrayDeque<GetSocketData>();\n        ArrayDeque<IdleSocketHolder> sockets = new ArrayDeque<IdleSocketHolder>();\n    }\n    Hashtable<String, ConnectionInfo> connectionInfo = new Hashtable<String, ConnectionInfo>();\n\n    int maxConnectionCount = Integer.MAX_VALUE;\n\n    public int getMaxConnectionCount() {\n        return maxConnectionCount;\n    }\n\n    public void setMaxConnectionCount(int maxConnectionCount) {\n        this.maxConnectionCount = maxConnectionCount;\n    }\n\n    @Override\n    public Cancellable getSocket(final GetSocketData data) {\n        final Uri uri = data.request.getUri();\n        final int port = getSchemePort(data.request.getUri());\n        if (port == -1) {\n            return null;\n        }\n\n        data.state.put(\"socket-owner\", this);\n\n        final String lookup = computeLookup(uri, port, data.request.getProxyHost(), data.request.getProxyPort());\n        ConnectionInfo info = getOrCreateConnectionInfo(lookup);\n        synchronized (AsyncSocketMiddleware.this) {\n            if (info.openCount >= maxConnectionCount) {\n                // wait for a connection queue to free up\n                SimpleCancellable queueCancel = new SimpleCancellable();\n                info.queue.add(data);\n                return queueCancel;\n            }\n\n            info.openCount++;\n\n            while (!info.sockets.isEmpty()) {\n                IdleSocketHolder idleSocketHolder = info.sockets.pop();\n                final AsyncSocket socket = idleSocketHolder.socket;\n                if (idleSocketHolder.idleTime + idleTimeoutMs < System.currentTimeMillis()) {\n                    socket.setClosedCallback(null);\n                    socket.close();\n                    continue;\n                }\n                if (!socket.isOpen())\n                    continue;\n\n                data.request.logd(\"Reusing keep-alive socket\");\n                data.connectCallback.onConnectCompleted(null, socket);\n\n                // just a noop/dummy, as this can't actually be cancelled.\n                SimpleCancellable ret = new SimpleCancellable();\n                ret.setComplete();\n                return ret;\n            }\n        }\n\n        if (!connectAllAddresses || proxyHost != null || data.request.getProxyHost() != null) {\n            // just default to connecting to a single address\n            data.request.logd(\"Connecting socket\");\n            String unresolvedHost;\n            int unresolvedPort;\n            boolean proxied = false;\n            if (data.request.getProxyHost() == null && proxyHost != null)\n                data.request.enableProxy(proxyHost, proxyPort);\n            if (data.request.getProxyHost() != null) {\n                unresolvedHost = data.request.getProxyHost();\n                unresolvedPort = data.request.getProxyPort();\n                proxied = true;\n            }\n            else {\n                unresolvedHost = uri.getHost();\n                unresolvedPort = port;\n            }\n            if (proxied) {\n                data.request.logv(\"Using proxy: \" + unresolvedHost + \":\" + unresolvedPort);\n            }\n            return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort,\n                wrapCallback(data, uri, port, proxied, data.connectCallback));\n        }\n\n        // try to connect to everything...\n        data.request.logv(\"Resolving domain and connecting to all available addresses\");\n\n        final SimpleFuture<AsyncSocket> checkedReturnValue = new SimpleFuture<>();\n\n        Future<AsyncSocket> socket = mClient.getServer().getAllByName(uri.getHost())\n        .then(addresses -> Futures.loopUntil(addresses, address -> {\n            SimpleFuture<AsyncSocket> loopResult = new SimpleFuture<>();\n\n            final String inetSockAddress = String.format(Locale.ENGLISH, \"%s:%s\", address, port);\n            data.request.logv(\"attempting connection to \" + inetSockAddress);\n\n            mClient.getServer().connectSocket(new InetSocketAddress(address, port), loopResult::setComplete);\n            return loopResult;\n        }))\n        // handle failures here (wrap the callback)\n        .fail(e -> wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(e, null));\n\n        checkedReturnValue.setComplete(socket)\n        .setCallback((e, successfulSocket) -> {\n            if (successfulSocket == null)\n                return;\n            // SimpleFuture.setComplete(Future) returns a future as to whether\n            // the completion was successful, or the future has been cancelled,\n            // thus the completion failed.\n            // The exception value will only ever be a CancellationException.\n            if (e == null) {\n                // handle successes here (wrap the callback)\n                wrapCallback(data, uri, port, false, data.connectCallback).onConnectCompleted(null, successfulSocket);\n                return;\n            }\n            data.request.logd(\"Recycling extra socket leftover from cancelled operation\");\n            idleSocket(successfulSocket);\n            recycleSocket(successfulSocket, data.request);\n        });\n\n        return checkedReturnValue;\n    }\n\n    private ConnectionInfo getOrCreateConnectionInfo(String lookup) {\n        ConnectionInfo info = connectionInfo.get(lookup);\n        if (info == null) {\n            info = new ConnectionInfo();\n            connectionInfo.put(lookup, info);\n        }\n        return info;\n    }\n\n    private void maybeCleanupConnectionInfo(String lookup) {\n        ConnectionInfo info = connectionInfo.get(lookup);\n        if (info == null)\n            return;\n        while (!info.sockets.isEmpty()) {\n            IdleSocketHolder idleSocketHolder = info.sockets.peekLast();\n            AsyncSocket socket = idleSocketHolder.socket;\n            if (idleSocketHolder.idleTime + idleTimeoutMs > System.currentTimeMillis())\n                break;\n            info.sockets.pop();\n            // remove the callback, prevent reentrancy.\n            socket.setClosedCallback(null);\n            socket.close();\n        }\n        if (info.openCount == 0 && info.queue.isEmpty() && info.sockets.isEmpty())\n            connectionInfo.remove(lookup);\n    }\n\n    private void recycleSocket(final AsyncSocket socket, AsyncHttpRequest request) {\n        if (socket == null)\n            return;\n        Uri uri = request.getUri();\n        int port = getSchemePort(uri);\n        final String lookup = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort());\n        final ArrayDeque<IdleSocketHolder> sockets;\n        final IdleSocketHolder idleSocketHolder = new IdleSocketHolder(socket);\n        synchronized (AsyncSocketMiddleware.this) {\n            ConnectionInfo info = getOrCreateConnectionInfo(lookup);\n            sockets = info.sockets;\n            sockets.push(idleSocketHolder);\n        }\n        socket.setClosedCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                synchronized (AsyncSocketMiddleware.this) {\n                    sockets.remove(idleSocketHolder);\n                    maybeCleanupConnectionInfo(lookup);\n                }\n            }\n        });\n    }\n\n    private void idleSocket(final AsyncSocket socket) {\n        // must listen for socket close, otherwise log will get spammed.\n        socket.setEndCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                socket.setClosedCallback(null);\n                socket.close();\n            }\n        });\n        socket.setWriteableCallback(null);\n        // should not get any data after this point...\n        // if so, eat it and disconnect.\n        socket.setDataCallback(new DataCallback.NullDataCallback() {\n            @Override\n            public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                super.onDataAvailable(emitter, bb);\n                bb.recycle();\n                socket.setClosedCallback(null);\n                socket.close();\n            }\n        });\n    }\n\n    private void nextConnection(AsyncHttpRequest request) {\n        Uri uri = request.getUri();\n        final int port = getSchemePort(uri);\n        String key = computeLookup(uri, port, request.getProxyHost(), request.getProxyPort());\n        synchronized (AsyncSocketMiddleware.this) {\n            ConnectionInfo info = connectionInfo.get(key);\n            if (info == null)\n                return;\n            --info.openCount;\n            while (info.openCount < maxConnectionCount && info.queue.size() > 0) {\n                GetSocketData gsd = info.queue.remove();\n                SimpleCancellable socketCancellable = (SimpleCancellable)gsd.socketCancellable;\n                if (socketCancellable.isCancelled())\n                    continue;\n                Cancellable connect = getSocket(gsd);\n                socketCancellable.setParent(connect);\n            }\n            maybeCleanupConnectionInfo(key);\n        }\n    }\n\n    protected boolean isKeepAlive(OnResponseCompleteData data) {\n        return HttpUtil.isKeepAlive(data.response.protocol(), data.response.headers()) && HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders());\n    }\n\n    @Override\n    public void onResponseComplete(final OnResponseCompleteData data) {\n        if (data.state.get(\"socket-owner\") != this)\n            return;\n\n        try {\n            idleSocket(data.socket);\n\n            if (data.exception != null || !data.socket.isOpen()) {\n                data.request.logv(\"closing out socket (exception)\");\n                data.socket.setClosedCallback(null);\n                data.socket.close();\n                return;\n            }\n            if (!isKeepAlive(data)) {\n                data.request.logv(\"closing out socket (not keep alive)\");\n                data.socket.setClosedCallback(null);\n                data.socket.close();\n                return;\n            }\n            data.request.logd(\"Recycling keep-alive socket\");\n            recycleSocket(data.socket, data.request);\n        }\n        finally {\n            nextConnection(data.request);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/BasicNameValuePair.java",
    "content": "/*\n * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/message/BasicNameValuePair.java $\n * $Revision: 604625 $\n * $Date: 2007-12-16 06:11:11 -0800 (Sun, 16 Dec 2007) $\n *\n * ====================================================================\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  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,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n * ====================================================================\n *\n * This software consists of voluntary contributions made by many\n * individuals on behalf of the Apache Software Foundation.  For more\n * information on the Apache Software Foundation, please see\n * <http://www.apache.org/>.\n *\n */\n\npackage com.jeffmony.async.http;\n\nimport android.text.TextUtils;\n\n/**\n * A simple class encapsulating an attribute/value pair.\n * <p>\n *  This class comforms to the generic grammar and formatting rules outlined in the \n *  <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2\">Section 2.2</a>\n *  and  \n *  <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6\">Section 3.6</a>\n *  of <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616.txt\">RFC 2616</a>\n * </p>\n * <h>2.2 Basic Rules</h>\n * <p>\n *  The following rules are used throughout this specification to describe basic parsing constructs. \n *  The US-ASCII coded character set is defined by ANSI X3.4-1986.\n * </p>\n * <pre>\n *     OCTET          = <any 8-bit sequence of data>\n *     CHAR           = <any US-ASCII character (octets 0 - 127)>\n *     UPALPHA        = <any US-ASCII uppercase letter \"A\"..\"Z\">\n *     LOALPHA        = <any US-ASCII lowercase letter \"a\"..\"z\">\n *     ALPHA          = UPALPHA | LOALPHA\n *     DIGIT          = <any US-ASCII digit \"0\"..\"9\">\n *     CTL            = <any US-ASCII control character\n *                      (octets 0 - 31) and DEL (127)>\n *     CR             = <US-ASCII CR, carriage return (13)>\n *     LF             = <US-ASCII LF, linefeed (10)>\n *     SP             = <US-ASCII SP, space (32)>\n *     HT             = <US-ASCII HT, horizontal-tab (9)>\n *     <\">            = <US-ASCII double-quote mark (34)>\n * </pre>\n * <p>\n *  Many HTTP/1.1 header field values consist of words separated by LWS or special \n *  characters. These special characters MUST be in a quoted string to be used within \n *  a parameter value (as defined in section 3.6).\n * <p>\n * <pre>\n * token          = 1*<any CHAR except CTLs or separators>\n * separators     = \"(\" | \")\" | \"<\" | \">\" | \"@\"\n *                | \",\" | \";\" | \":\" | \"\\\" | <\">\n *                | \"/\" | \"[\" | \"]\" | \"?\" | \"=\"\n *                | \"{\" | \"}\" | SP | HT\n * </pre>\n * <p>\n *  A string of text is parsed as a single word if it is quoted using double-quote marks.\n * </p>\n * <pre>\n * quoted-string  = ( <\"> *(qdtext | quoted-pair ) <\"> )\n * qdtext         = <any TEXT except <\">>\n * </pre>\n * <p>\n *  The backslash character (\"\\\") MAY be used as a single-character quoting mechanism only \n *  within quoted-string and comment constructs.\n * </p>\n * <pre>\n * quoted-pair    = \"\\\" CHAR\n * </pre>\n * <h>3.6 Transfer Codings</h>\n * <p>\n *  Parameters are in the form of attribute/value pairs.\n * </p>\n * <pre>\n * parameter               = attribute \"=\" value\n * attribute               = token\n * value                   = token | quoted-string\n * </pre> \n * \n * @author <a href=\"mailto:oleg at ural.com\">Oleg Kalnichevski</a>\n * \n */\npublic class BasicNameValuePair implements NameValuePair, Cloneable {\n\n    private final String name;\n    private final String value;\n\n    /**\n     * Default Constructor taking a name and a value. The value may be null.\n     * \n     * @param name The name.\n     * @param value The value.\n     */\n    public BasicNameValuePair(final String name, final String value) {\n        super();\n        if (name == null) {\n            throw new IllegalArgumentException(\"Name may not be null\");\n        }\n        this.name = name;\n        this.value = value;\n    }\n\n    /**\n     * Returns the name.\n     *\n     * @return String name The name\n     */\n    public String getName() {\n        return this.name;\n    }\n\n    /**\n     * Returns the value.\n     *\n     * @return String value The current value.\n     */\n    public String getValue() {\n        return this.value;\n    }\n\n    \n    /**\n     * Get a string representation of this pair.\n     * \n     * @return A string representation.\n     */\n    public String toString() {\n        return name + \"=\" + value;\n    }\n\n    public boolean equals(final Object object) {\n        if (object == null) return false;\n        if (this == object) return true;\n        if (object instanceof NameValuePair) {\n            BasicNameValuePair that = (BasicNameValuePair) object;\n            return this.name.equals(that.name)\n                  && TextUtils.equals(this.value, that.value);\n        } else {\n            return false;\n        }\n    }\n\n    public int hashCode() {\n        return name.hashCode() ^ value.hashCode();\n    }\n    \n    public Object clone() throws CloneNotSupportedException {\n        return super.clone();\n    }\n \n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/BodyDecoderException.java",
    "content": "package com.jeffmony.async.http;\n\npublic class BodyDecoderException extends Exception {\n    public BodyDecoderException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/ConnectionClosedException.java",
    "content": "package com.jeffmony.async.http;\n\npublic class ConnectionClosedException extends Exception {\n    public ConnectionClosedException(String message) {\n        super(message);\n    }\n\n    public ConnectionClosedException(String detailMessage, Throwable throwable) {\n        super(detailMessage, throwable);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/ConnectionFailedException.java",
    "content": "package com.jeffmony.async.http;\n\npublic class ConnectionFailedException extends Exception {\n    public ConnectionFailedException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/Headers.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.text.TextUtils;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\n/**\n * Created by koush on 7/21/14.\n */\npublic class Headers {\n    public Headers() {\n    }\n\n    public Headers(Map<String, List<String>> mm) {\n        for (String key: mm.keySet()) {\n            addAll(key, mm.get(key));\n        }\n    }\n\n    final Multimap map = new Multimap() {\n        @Override\n        protected List<String> newList() {\n            return new TaggedList<String>();\n        }\n    };\n    public Multimap getMultiMap() {\n        return map;\n    }\n\n    public List<String> getAll(String header) {\n        return map.get(header.toLowerCase(Locale.US));\n    }\n\n    public String get(String header) {\n        return map.getString(header.toLowerCase(Locale.US));\n    }\n\n    public Headers set(String header, String value) {\n        if (value != null && (value.contains(\"\\n\") || value.contains(\"\\r\")))\n            throw new IllegalArgumentException(\"value must not contain a new line or line feed\");\n        String lc = header.toLowerCase(Locale.US);\n        map.put(lc, value);\n        TaggedList<String> list = (TaggedList<String>)map.get(lc);\n        list.tagNull(header);\n        return this;\n    }\n\n    public Headers add(String header, String value) {\n        String lc = header.toLowerCase(Locale.US);\n        map.add(lc, value);\n        TaggedList<String> list = (TaggedList<String>)map.get(lc);\n        list.tagNull(header);\n        return this;\n    }\n\n    public Headers addLine(String line) {\n        if (line != null) {\n            line = line.trim();\n            String[] parts = line.split(\":\", 2);\n            if (parts.length == 2)\n                add(parts[0].trim(), parts[1].trim());\n            else\n                add(parts[0].trim(), \"\");\n        }\n        return this;\n    }\n\n    public Headers addAll(String header, List<String> values) {\n        for (String v: values) {\n            add(header, v);\n        }\n        return this;\n    }\n\n    public Headers addAll(Map<String, List<String>> m) {\n        for (String key: m.keySet()) {\n            for (String value: m.get(key)) {\n                add(key, value);\n            }\n        }\n        return this;\n    }\n\n    public Headers addAllMap(Map<String, String> m) {\n        for (String key: m.keySet()) {\n            add(key, m.get(key));\n        }\n        return this;\n    }\n\n    public Headers addAll(Headers headers) {\n        // safe to addall since this is another Headers object\n        map.putAll(headers.map);\n        return this;\n    }\n\n    public List<String> removeAll(String header) {\n        return map.remove(header.toLowerCase(Locale.US));\n    }\n\n    public String remove(String header) {\n        List<String> r = removeAll(header.toLowerCase(Locale.US));\n        if (r == null || r.size() == 0)\n            return null;\n        return r.get(0);\n    }\n\n    public Headers removeAll(Collection<String> headers) {\n        for (String header: headers) {\n            remove(header);\n        }\n        return this;\n    }\n\n    public StringBuilder toStringBuilder() {\n        StringBuilder result = new StringBuilder(256);\n        for (String key: map.keySet()) {\n            TaggedList<String> list = (TaggedList<String>)map.get(key);\n            for (String v: list) {\n                result.append((String)list.tag())\n                .append(\": \")\n                .append(v)\n                .append(\"\\r\\n\");\n            }\n        }\n        result.append(\"\\r\\n\");\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return toStringBuilder().toString();\n    }\n\n    public String toPrefixString(String prefix) {\n        return\n        toStringBuilder()\n        .insert(0, prefix + \"\\r\\n\")\n        .toString();\n    }\n\n    public static Headers parse(String payload) {\n        String[] lines = payload.split(\"\\n\");\n\n        Headers headers = new Headers();\n        for (String line: lines) {\n            line = line.trim();\n            if (TextUtils.isEmpty(line))\n                continue;\n\n            headers.addLine(line);\n        }\n        return headers;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/HttpDate.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.http;\n\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\n/**\n * Best-effort parser for HTTP dates.\n */\npublic final class HttpDate {\n\n    /**\n     * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such\n     * cookies are on the fast path.\n     */\n    private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT\n            = new ThreadLocal<DateFormat>() {\n        @Override\n        protected DateFormat initialValue() {\n            DateFormat rfc1123 = new SimpleDateFormat(\"EEE, dd MMM yyyy HH:mm:ss zzz\", Locale.US);\n            rfc1123.setTimeZone(TimeZone.getTimeZone(\"UTC\"));\n            return rfc1123;\n        }\n    };\n\n    /**\n     * If we fail to parse a date in a non-standard format, try each of these formats in sequence.\n     */\n    private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {\n            /* This list comes from  {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */\n            \"EEEE, dd-MMM-yy HH:mm:ss zzz\", // RFC 1036\n            \"EEE MMM d HH:mm:ss yyyy\", // ANSI C asctime()\n            \"EEE, dd-MMM-yyyy HH:mm:ss z\",\n            \"EEE, dd-MMM-yyyy HH-mm-ss z\",\n            \"EEE, dd MMM yy HH:mm:ss z\",\n            \"EEE dd-MMM-yyyy HH:mm:ss z\",\n            \"EEE dd MMM yyyy HH:mm:ss z\",\n            \"EEE dd-MMM-yyyy HH-mm-ss z\",\n            \"EEE dd-MMM-yy HH:mm:ss z\",\n            \"EEE dd MMM yy HH:mm:ss z\",\n            \"EEE,dd-MMM-yy HH:mm:ss z\",\n            \"EEE,dd-MMM-yyyy HH:mm:ss z\",\n            \"EEE, dd-MM-yyyy HH:mm:ss z\",\n\n            /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */\n            \"EEE MMM d yyyy HH:mm:ss z\",\n    };\n\n    /**\n     * Returns the date for {@code value}. Returns null if the value couldn't be\n     * parsed.\n     */\n    public static Date parse(String value) {\n        if (value == null)\n            return null;\n        try {\n            return STANDARD_DATE_FORMAT.get().parse(value);\n        } catch (ParseException ignore) {\n        }\n        for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {\n            try {\n                return new SimpleDateFormat(formatString, Locale.US).parse(value);\n            } catch (ParseException ignore) {\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Returns the string for {@code value}.\n     */\n    public static String format(Date value) {\n        return STANDARD_DATE_FORMAT.get().format(value);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/HttpTransportMiddleware.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.BufferedDataSink;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.LineEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\nimport com.jeffmony.async.http.filter.ChunkedOutputFilter;\n\nimport java.io.IOException;\n\n/**\n * Created by koush on 7/24/14.\n */\npublic class HttpTransportMiddleware extends SimpleMiddleware {\n    @Override\n    public boolean exchangeHeaders(final OnExchangeHeaderData data) {\n        Protocol p = Protocol.get(data.protocol);\n        if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1)\n            return super.exchangeHeaders(data);\n\n        AsyncHttpRequest request = data.request;\n        AsyncHttpRequestBody requestBody = data.request.getBody();\n\n        if (requestBody != null) {\n            if (requestBody.length() >= 0) {\n                request.getHeaders().set(\"Content-Length\", String.valueOf(requestBody.length()));\n                data.response.sink(data.socket);\n            }\n            else if (\"close\".equals(request.getHeaders().get(\"Connection\"))) {\n                data.response.sink(data.socket);\n            }\n            else {\n                request.getHeaders().set(\"Transfer-Encoding\", \"Chunked\");\n                data.response.sink(new ChunkedOutputFilter(data.socket));\n            }\n        }\n\n        String rl = request.getRequestLine().toString();\n        String rs = request.getHeaders().toPrefixString(rl);\n\n        byte[] rsBytes = rs.getBytes();\n\n        // try to get the request body in the same packet as the request headers... if it will fit\n        // in the max MTU (1540 or whatever).\n        final boolean waitForBody = requestBody != null && requestBody.length() >= 0 && requestBody.length() + rsBytes.length < 1024;\n        final BufferedDataSink bsink;\n        final DataSink headerSink;\n        if (waitForBody) {\n            // force buffering of headers\n            bsink = new BufferedDataSink(data.response.sink());\n            bsink.forceBuffering(true);\n            data.response.sink(bsink);\n            headerSink = bsink;\n        }\n        else {\n            bsink = null;\n            headerSink = data.socket;\n        }\n\n        request.logv(\"\\n\" + rs);\n\n        final CompletedCallback sentCallback = data.sendHeadersCallback;\n        Util.writeAll(headerSink, rsBytes, new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                Util.end(sentCallback, ex);\n                // flush headers and any request body that was written by the callback\n                if (bsink != null) {\n                    bsink.forceBuffering(false);\n                    bsink.setMaxBuffer(0);\n                }\n            }\n        });\n\n        LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() {\n            Headers mRawHeaders = new Headers();\n            String statusLine;\n\n            @Override\n            public void onStringAvailable(String s) {\n                try {\n                    s = s.trim();\n                    if (statusLine == null) {\n                        statusLine = s;\n                    }\n                    else if (!TextUtils.isEmpty(s)) {\n                        mRawHeaders.addLine(s);\n                    }\n                    else {\n                        String[] parts = statusLine.split(\" \", 3);\n                        if (parts.length < 2)\n                            throw new Exception(new IOException(\"Not HTTP\"));\n\n                        data.response.headers(mRawHeaders);\n                        String protocol = parts[0];\n                        data.response.protocol(protocol);\n                        data.response.code(Integer.parseInt(parts[1]));\n                        data.response.message(parts.length == 3 ? parts[2] : \"\");\n                        data.receiveHeadersCallback.onCompleted(null);\n\n                        // socket may get detached after headers (websocket)\n                        AsyncSocket socket = data.response.socket();\n                        if (socket == null)\n                            return;\n                        DataEmitter emitter;\n                        // HEAD requests must not return any data. They still may\n                        // return content length, etc, which will confuse the body decoder\n                        if (!data.request.hasBody()) {\n                            emitter = HttpUtil.EndEmitter.create(socket.getServer(), null);\n                        }\n                        else if (responseIsEmpty(data.response.code())) {\n                            emitter = HttpUtil.EndEmitter.create(socket.getServer(), null);\n                        }\n                        else {\n                            emitter = HttpUtil.getBodyDecoder(socket, Protocol.get(protocol), mRawHeaders, false);\n                        }\n                        data.response.emitter(emitter);\n                    }\n                }\n                catch (Exception ex) {\n                    data.receiveHeadersCallback.onCompleted(ex);\n                }\n            }\n        };\n\n        LineEmitter liner = new LineEmitter();\n        data.socket.setDataCallback(liner);\n        liner.setLineCallback(headerCallback);\n        return true;\n    }\n\n    static boolean responseIsEmpty(int code) {\n        return (code >= 100 && code <= 199) || code == 204 || code == 304;\n    }\n\n    @Override\n    public void onRequestSent(OnRequestSentData data) {\n        Protocol p = Protocol.get(data.protocol);\n        if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1)\n            return;\n\n        if (data.response.sink() instanceof ChunkedOutputFilter)\n            data.response.sink().end();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/HttpUtil.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\nimport com.jeffmony.async.http.body.JSONObjectBody;\nimport com.jeffmony.async.http.body.MultipartFormDataBody;\nimport com.jeffmony.async.http.body.StringBody;\nimport com.jeffmony.async.http.body.UrlEncodedFormBody;\nimport com.jeffmony.async.http.filter.ChunkedInputFilter;\nimport com.jeffmony.async.http.filter.ContentLengthFilter;\nimport com.jeffmony.async.http.filter.GZIPInputFilter;\nimport com.jeffmony.async.http.filter.InflaterInputFilter;\n\npublic class HttpUtil {\n    public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, Headers headers) {\n        String contentType = headers.get(\"Content-Type\");\n        if (contentType != null) {\n            String[] values = contentType.split(\";\");\n            for (int i = 0; i < values.length; i++) {\n                values[i] = values[i].trim();\n            }\n            for (String ct: values) {\n                if (UrlEncodedFormBody.CONTENT_TYPE.equals(ct)) {\n                    return new UrlEncodedFormBody();\n                }\n                if (JSONObjectBody.CONTENT_TYPE.equals(ct)) {\n                    return new JSONObjectBody();\n                }\n                if (StringBody.CONTENT_TYPE.equals(ct)) {\n                    return new StringBody();\n                }\n                if (ct != null && ct.startsWith(MultipartFormDataBody.PRIMARY_TYPE)) {\n                    return new MultipartFormDataBody(contentType);\n                }\n            }\n        }\n\n        return null;\n    }\n    \n    static class EndEmitter extends FilteredDataEmitter {\n        private EndEmitter() {\n        }\n        \n        public static EndEmitter create(AsyncServer server, final Exception e) {\n            final EndEmitter ret = new EndEmitter();\n            // don't need to worry about any race conditions with post and this return value\n            // since we are in the server thread.\n            server.post(new Runnable() {\n                @Override\n                public void run() {\n                    ret.report(e);\n                }\n            });\n            return ret;\n        }\n    }\n    \n    public static DataEmitter getBodyDecoder(DataEmitter emitter, Protocol protocol, Headers headers, boolean server) {\n        long _contentLength = -1;\n        try {\n            String header = headers.get(\"Content-Length\");\n            if (header != null)\n                _contentLength = Long.parseLong(header);\n        }\n        catch (NumberFormatException ex) {\n        }\n        final long contentLength = _contentLength;\n        if (-1 != contentLength) {\n            if (contentLength < 0) {\n                EndEmitter ender = EndEmitter.create(emitter.getServer(), new BodyDecoderException(\"not using chunked encoding, and no content-length found.\"));\n                ender.setDataEmitter(emitter);\n                emitter = ender;\n                return emitter;\n            }\n            if (contentLength == 0) {\n                EndEmitter ender = EndEmitter.create(emitter.getServer(), null);\n                ender.setDataEmitter(emitter);\n                emitter = ender;\n                return emitter;\n            }\n            ContentLengthFilter contentLengthWatcher = new ContentLengthFilter(contentLength);\n            contentLengthWatcher.setDataEmitter(emitter);\n            emitter = contentLengthWatcher;\n        }\n        else if (\"chunked\".equalsIgnoreCase(headers.get(\"Transfer-Encoding\"))) {\n            ChunkedInputFilter chunker = new ChunkedInputFilter();\n            chunker.setDataEmitter(emitter);\n            emitter = chunker;\n        }\n        else if (server) {\n            // if this is the server, and the client has not indicated a request body, the client is done\n            EndEmitter ender = EndEmitter.create(emitter.getServer(), null);\n            ender.setDataEmitter(emitter);\n            emitter = ender;\n            return emitter;\n        }\n\n        if (\"gzip\".equals(headers.get(\"Content-Encoding\"))) {\n            GZIPInputFilter gunzipper = new GZIPInputFilter();\n            gunzipper.setDataEmitter(emitter);\n            emitter = gunzipper;\n        }        \n        else if (\"deflate\".equals(headers.get(\"Content-Encoding\"))) {\n            InflaterInputFilter inflater = new InflaterInputFilter();\n            inflater.setDataEmitter(emitter);\n            emitter = inflater;\n        }\n\n        // conversely, if this is the client (http 1.0), and the server has not indicated a request body, we do not report\n        // the close/end event until the server actually closes the connection.\n        return emitter;\n    }\n\n    public static boolean isKeepAlive(Protocol protocol, Headers headers) {\n        // connection is always keep alive as this is an http/1.1 client\n        String connection = headers.get(\"Connection\");\n        if (connection == null)\n            return protocol == Protocol.HTTP_1_1;\n        return \"keep-alive\".equalsIgnoreCase(connection);\n    }\n\n    public static boolean isKeepAlive(String protocol, Headers headers) {\n        // connection is always keep alive as this is an http/1.1 client\n        String connection = headers.get(\"Connection\");\n        if (connection == null)\n            return Protocol.get(protocol) == Protocol.HTTP_1_1;\n        return \"keep-alive\".equalsIgnoreCase(connection);\n    }\n\n    public static long contentLength(Headers headers) {\n        String cl = headers.get(\"Content-Length\");\n        if (cl == null)\n            return -1;\n        try {\n            return Long.parseLong(cl);\n        }\n        catch (NumberFormatException e) {\n            return -1;\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/HybiParser.java",
    "content": "//\n// HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser\n//\n// Based on code from the faye project.\n// https://github.com/faye/faye-websocket-node\n// Copyright (c) 2009-2012 James Coglan\n//\n// Ported from Javascript to Java by Eric Butler <eric@codebutler.com>\n//\n// (The MIT License)\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage com.jeffmony.async.http;\n\nimport android.util.Log;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataEmitterReader;\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.zip.DataFormatException;\nimport java.util.zip.Inflater;\n\nabstract class HybiParser {\n    private static final String TAG = \"HybiParser\";\n\n    private boolean mMasking = true;\n    private boolean mDeflate = false;\n\n    private int     mStage;\n\n    private boolean mFinal;\n    private boolean mMasked;\n    private boolean mDeflated;\n    private int     mOpcode;\n    private int     mLengthSize;\n    private int     mLength;\n    private int     mMode;\n\n    private byte[] mMask    = new byte[0];\n    private byte[] mPayload = new byte[0];\n\n    private boolean mClosed = false;\n\n    private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();\n    private Inflater mInflater = new Inflater(true);\n    private byte[] mInflateBuffer = new byte[4096];\n\n    private static final int BYTE   = 255;\n    private static final int FIN    = 128;\n    private static final int MASK   = 128;\n    private static final int RSV1   =  64;\n    private static final int RSV2   =  32;\n    private static final int RSV3   =  16;\n    private static final int OPCODE =  15;\n    private static final int LENGTH = 127;\n\n    private static final int MODE_TEXT   = 1;\n    private static final int MODE_BINARY = 2;\n\n    private static final int OP_CONTINUATION =  0;\n    private static final int OP_TEXT         =  1;\n    private static final int OP_BINARY       =  2;\n    private static final int OP_CLOSE        =  8;\n    private static final int OP_PING         =  9;\n    private static final int OP_PONG         = 10;\n\n    private static final List<Integer> OPCODES = Arrays.asList(\n        OP_CONTINUATION,\n        OP_TEXT,\n        OP_BINARY,\n        OP_CLOSE,\n        OP_PING,\n        OP_PONG\n    );\n\n    private static final List<Integer> FRAGMENTED_OPCODES = Arrays.asList(\n        OP_CONTINUATION, OP_TEXT, OP_BINARY\n    );\n//\n//    public HybiParser(WebSocketClient client) {\n//        mClient = client;\n//    }\n\n    private static byte[] mask(byte[] payload, byte[] mask, int offset) {\n        if (mask.length == 0) return payload;\n\n        for (int i = 0; i < payload.length - offset; i++) {\n            payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]);\n        }\n        return payload;\n    }\n\n    private byte[] inflate(byte[] payload) throws DataFormatException {\n        ByteArrayOutputStream inflated = new ByteArrayOutputStream();\n\n        mInflater.setInput(payload);\n        while (!mInflater.needsInput()) {\n            int chunkSize = mInflater.inflate(mInflateBuffer);\n            inflated.write(mInflateBuffer, 0, chunkSize);\n        }\n\n        mInflater.setInput(new byte[] { 0, 0, -1, -1 });\n        while (!mInflater.needsInput()) {\n            int chunkSize = mInflater.inflate(mInflateBuffer);\n            inflated.write(mInflateBuffer, 0, chunkSize);\n        }\n\n        return inflated.toByteArray();\n    }\n\n    public void setMasking(boolean masking) {\n        mMasking = masking;\n    }\n\n    public void setDeflate(boolean deflate) {\n        mDeflate = deflate;\n    }\n\n    DataCallback mStage0 = new DataCallback() {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            try {\n                parseOpcode(bb.get());\n            }\n            catch (ProtocolError e) {\n                report(e);\n                e.printStackTrace();\n            }\n            parse();\n        }\n    };\n\n    DataCallback mStage1 = new DataCallback() {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            parseLength(bb.get());\n            parse();\n        }\n    };\n\n    DataCallback mStage2 = new DataCallback() {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            byte[] bytes = new byte[mLengthSize];\n            bb.get(bytes);\n            try {\n                parseExtendedLength(bytes);\n            }\n            catch (ProtocolError e) {\n                report(e);\n                e.printStackTrace();\n            }\n            parse();\n        }\n    };\n\n    DataCallback mStage3 = new DataCallback() {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            mMask = new byte[4];\n            bb.get(mMask);\n            mStage = 4;\n            parse();\n        }\n    };\n\n    DataCallback mStage4 = new DataCallback() {\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            assert bb.remaining() == mLength;\n            mPayload = new byte[mLength];\n            bb.get(mPayload);\n            try {\n                emitFrame();\n            }\n            catch (IOException e) {\n                report(e);\n                e.printStackTrace();\n            }\n            mStage = 0;\n            parse();\n        }\n    };\n\n    void parse() {\n        switch (mStage) {\n        case 0:\n            mReader.read(1, mStage0);\n            break;\n        case 1:\n            mReader.read(1, mStage1);\n            break;\n        case 2:\n            mReader.read(mLengthSize, mStage2);\n            break;\n        case 3:\n            mReader.read(4, mStage3);\n            break;\n        case 4:\n            mReader.read(mLength, mStage4);\n            break;\n        }\n    }\n\n    private DataEmitterReader mReader = new DataEmitterReader();\n\n\tprivate static final long BASE = 2;\n\n\tprivate static final long _2_TO_8_ = BASE << 7;\n\n\tprivate static final long _2_TO_16_ = BASE << 15;\n\n\tprivate static final long _2_TO_24 = BASE << 23;\n\n\tprivate static final long _2_TO_32_ = BASE << 31;\n\n\tprivate static final long _2_TO_40_ = BASE << 39;\n\n\tprivate static final long _2_TO_48_ = BASE << 47;\n\n\tprivate static final long _2_TO_56_ = BASE << 55;\n    public HybiParser(DataEmitter socket) {\n        socket.setDataCallback(mReader);\n        parse();\n    }\n\n    private void parseOpcode(byte data) throws ProtocolError {\n        boolean rsv1 = (data & RSV1) == RSV1;\n        boolean rsv2 = (data & RSV2) == RSV2;\n        boolean rsv3 = (data & RSV3) == RSV3;\n\n        if ((!mDeflate && rsv1) || rsv2 || rsv3) {\n            throw new ProtocolError(\"RSV not zero\");\n        }\n\n        mFinal   = (data & FIN) == FIN;\n        mOpcode  = (data & OPCODE);\n        mDeflated = rsv1;\n        mMask    = new byte[0];\n        mPayload = new byte[0];\n\n        if (!OPCODES.contains(mOpcode)) {\n            throw new ProtocolError(\"Bad opcode\");\n        }\n\n        if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {\n            throw new ProtocolError(\"Expected non-final packet\");\n        }\n\n        mStage = 1;\n    }\n\n    private void parseLength(byte data) {\n        mMasked = (data & MASK) == MASK;\n        mLength = (data & LENGTH);\n\n        if (mLength >= 0 && mLength <= 125) {\n            mStage = mMasked ? 3 : 4;\n        } else {\n            mLengthSize = (mLength == 126) ? 2 : 8;\n            mStage      = 2;\n        }\n    }\n\n    private void parseExtendedLength(byte[] buffer) throws ProtocolError {\n        mLength = getInteger(buffer);\n        mStage  = mMasked ? 3 : 4;\n    }\n\n    public byte[] frame(String data) {\n        return frame(OP_TEXT, data, -1);\n    }\n\n    public byte[] frame(byte[] data) {\n        return frame(OP_BINARY, data, -1);\n    }\n\n    public byte[] frame(byte[] data, int offset, int length) {\n    \treturn frame(OP_BINARY, data, -1, offset, length);\n    }\n\n    public byte[] pingFrame(String data) {\n        return frame(OP_PING, data, -1);\n    }\n\n    public byte[] pongFrame(String data) {\n        return frame(OP_PONG, data, -1);\n    }\n\n    /**\n     * Flip the opcode so to avoid the name collision with the public method\n     *\n     * @param opcode\n     * @param data\n     * @param errorCode\n     * @return\n     */\n    private byte[] frame(int opcode, byte[] data, int errorCode)  {\n        return frame(opcode, data, errorCode, 0, data.length);\n    }\n\n    /**\n     * Don't actually need the flipped method signature, trying to keep it in line with the byte[] version\n     *\n     * @param opcode\n     * @param data\n     * @param errorCode\n     * @return\n     */\n    private byte[] frame(int opcode, String data, int errorCode) {\n        return frame(opcode, decode(data), errorCode);\n    }\n\n    private byte[] frame(int opcode, byte [] data, int errorCode, int dataOffset, int dataLength) {\n        if (mClosed) return null;\n\n//        Log.d(TAG, \"Creating frame for: \" + data + \" op: \" + opcode + \" err: \" + errorCode);\n        byte[] buffer = data;\n        int insert = (errorCode > 0) ? 2 : 0;\n        int length = dataLength + insert - dataOffset;\n        int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10);\n        int offset = header + (mMasking ? 4 : 0);\n        int masked = mMasking ? MASK : 0;\n        byte[] frame = new byte[length + offset];\n\n        frame[0] = (byte) ((byte)FIN | (byte)opcode);\n\n        if (length <= 125) {\n            frame[1] = (byte) (masked | length);\n        } else if (length <= 65535) {\n            frame[1] = (byte) (masked | 126);\n            frame[2] = (byte) (length / 256);\n            frame[3] = (byte) (length & BYTE);\n        } else {\n\n        \tframe[1] = (byte) (masked | 127);\n            frame[2] = (byte) (( length / _2_TO_56_) & BYTE);\n            frame[3] = (byte) (( length / _2_TO_48_) & BYTE);\n            frame[4] = (byte) (( length / _2_TO_40_) & BYTE);\n            frame[5] = (byte) (( length / _2_TO_32_) & BYTE);\n            frame[6] = (byte) (( length / _2_TO_24) & BYTE);\n            frame[7] = (byte) (( length / _2_TO_16_) & BYTE);\n            frame[8] = (byte) (( length / _2_TO_8_)  & BYTE);\n            frame[9] = (byte) (length & BYTE);\n        }\n\n        if (errorCode > 0) {\n            frame[offset] = (byte) ((errorCode / 256) & BYTE);\n            frame[offset+1] = (byte) (errorCode & BYTE);\n        }\n\n        System.arraycopy(buffer, dataOffset, frame, offset + insert, dataLength - dataOffset);\n\n        if (mMasking) {\n            byte[] mask = {\n                (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256),\n                (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256)\n            };\n            System.arraycopy(mask, 0, frame, header, mask.length);\n            mask(frame, mask, offset);\n        }\n\n        return frame;\n    }\n\n    public void close(int code, String reason) {\n        if (mClosed) return;\n        sendFrame(frame(OP_CLOSE, reason, code));\n        mClosed = true;\n    }\n\n    private void emitFrame() throws IOException {\n        byte[] payload = mask(mPayload, mMask, 0);\n        if (mDeflated) {\n            try {\n                payload = inflate(payload);\n            } catch (DataFormatException e) {\n                throw new IOException(\"Invalid deflated data\");\n            }\n        }\n        int opcode = mOpcode;\n\n        if (opcode == OP_CONTINUATION) {\n            if (mMode == 0) {\n                throw new ProtocolError(\"Mode was not set.\");\n            }\n            mBuffer.write(payload);\n            if (mFinal) {\n                byte[] message = mBuffer.toByteArray();\n                if (mMode == MODE_TEXT) {\n                    onMessage(encode(message));\n                } else {\n                    onMessage(message);\n                }\n                reset();\n            }\n\n        } else if (opcode == OP_TEXT) {\n            if (mFinal) {\n                String messageText = encode(payload);\n                onMessage(messageText);\n            } else {\n                mMode = MODE_TEXT;\n                mBuffer.write(payload);\n            }\n\n        } else if (opcode == OP_BINARY) {\n            if (mFinal) {\n                onMessage(payload);\n            } else {\n                mMode = MODE_BINARY;\n                mBuffer.write(payload);\n            }\n\n        } else if (opcode == OP_CLOSE) {\n            int    code   = (payload.length >= 2) ? 256 * (payload[0] & 0xFF) + (payload[1] & 0xFF) : 0;\n            String reason = (payload.length >  2) ? encode(slice(payload, 2))     : null;\n//            Log.d(TAG, \"Got close op! \" + code + \" \" + reason);\n            onDisconnect(code, reason);\n\n        } else if (opcode == OP_PING) {\n            if (payload.length > 125) { throw new ProtocolError(\"Ping payload too large\"); }\n//            Log.d(TAG, \"Sending pong!!\");\n            String message = encode(payload);\n            sendFrame(frame(OP_PONG, payload, -1));\n            onPing(message);\n        } else if (opcode == OP_PONG) {\n            String message = encode(payload);\n            onPong(message);\n//            Log.d(TAG, \"Got pong! \" + message);\n        }\n    }\n\n    protected abstract void onMessage(byte[] payload);\n    protected abstract void onMessage(String payload);\n    protected abstract void onPong(String payload);\n    protected abstract void onPing(String payload);\n    protected abstract void onDisconnect(int code, String reason);\n    protected abstract void report(Exception ex);\n\n    protected abstract void sendFrame(byte[] frame);\n\n    private void reset() {\n        mMode = 0;\n        mBuffer.reset();\n    }\n\n    private String encode(byte[] buffer) {\n        try {\n            return new String(buffer, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private byte[] decode(String string) {\n        try {\n            return (string).getBytes(\"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private int getInteger(byte[] bytes) throws ProtocolError {\n        long i = byteArrayToLong(bytes, 0, bytes.length);\n        if (i < 0 || i > Integer.MAX_VALUE) {\n            throw new ProtocolError(\"Bad integer: \" + i);\n        }\n        return (int) i;\n    }\n\n    private byte[] slice(byte[] array, int start) {\n        byte[] copy = new byte[array.length - start];\n        System.arraycopy(array, start, copy, 0, array.length - start);\n        return copy;\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        Inflater inflater = mInflater;\n\n        if (inflater != null) {\n            try {\n                inflater.end();\n            } catch (Exception e) {\n                Log.e(TAG, \"inflater.end failed\", e);\n            }\n        }\n\n        super.finalize();\n    }\n\n    public static class ProtocolError extends IOException {\n        public ProtocolError(String detailMessage) {\n            super(detailMessage);\n        }\n    }\n\n    private static long byteArrayToLong(byte[] b, int offset, int length) {\n        if (b.length < length)\n            throw new IllegalArgumentException(\"length must be less than or equal to b.length\");\n\n        long value = 0;\n        for (int i = 0; i < length; i++) {\n            int shift = (length - 1 - i) * 8;\n            value += (b[i + offset] & 0x000000FF) << shift;\n        }\n        return value;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/Multimap.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class Multimap extends LinkedHashMap<String, List<String>> implements Iterable<NameValuePair> {\n    public Multimap() {\n    }\n\n    protected List<String> newList() {\n        return new ArrayList<String>();\n    }\n\n    public String getString(String name) {\n        List<String> ret = get(name);\n        if (ret == null || ret.size() == 0)\n            return null;\n        return ret.get(0);\n    }\n\n    public String getAllString(String name, String delimiter) {\n        List<String> ret = get(name);\n        if (ret == null || ret.size() == 0)\n            return null;\n        StringBuilder builder = new StringBuilder();\n        boolean first = true;\n        for (String value: ret) {\n            if (!first)\n                builder.append(delimiter);\n\n            builder.append(value);\n            first = false;\n        }\n        return builder.toString();\n    }\n\n    public List<String> ensure(String name) {\n        List<String> ret = get(name);\n        if (ret == null) {\n            ret = newList();\n            put(name, ret);\n        }\n        return ret;\n    }\n\n    public void add(String name, String value) {\n        ensure(name).add(value);\n    }\n\n    public void put(String name, String value) {\n        List<String> ret = newList();\n        ret.add(value);\n        put(name, ret);\n    }\n\n    public Multimap(List<NameValuePair> pairs) {\n        for (NameValuePair pair: pairs)\n            add(pair.getName(), pair.getValue());\n    }\n\n    public Multimap(Multimap m) {\n        putAll(m);\n    }\n\n    public interface StringDecoder {\n        public String decode(String s);\n    }\n\n    public static Multimap parse(String value, String delimiter, boolean unquote, StringDecoder decoder) {\n        return parse(value, delimiter, \"=\", unquote, decoder);\n    }\n\n    public static Multimap parse(String value, String delimiter, String assigner, boolean unquote, StringDecoder decoder) {\n        Multimap map = new Multimap();\n        if (value == null)\n            return map;\n        String[] parts = value.split(delimiter);\n        for (String part: parts) {\n            String[] pair = part.split(assigner, 2);\n            String key = pair[0].trim();\n            // watch for empty string or trailing delimiter\n            if (TextUtils.isEmpty(key))\n                continue;\n            String v = null;\n            if (pair.length > 1)\n                v = pair[1];\n            if (v != null && unquote && v.endsWith(\"\\\"\") && v.startsWith(\"\\\"\"))\n                v = v.substring(1, v.length() - 1);\n            if (v != null && decoder != null) {\n                key = decoder.decode(key);\n                v = decoder.decode(v);\n            }\n            map.add(key, v);\n        }\n        return map;\n    }\n\n    public static Multimap parseSemicolonDelimited(String header) {\n        return parse(header, \";\", true, null);\n    }\n\n    public static Multimap parseCommaDelimited(String header) {\n        return parse(header, \",\", true, null);\n    }\n\n    public static final StringDecoder QUERY_DECODER = new StringDecoder() {\n        @Override\n        public String decode(String s) {\n            return Uri.decode(s);\n        }\n    };\n\n    public static Multimap parseQuery(String query) {\n        return parse(query, \"&\", false, QUERY_DECODER);\n    }\n\n    public static final StringDecoder URL_DECODER = new StringDecoder() {\n        @Override\n        public String decode(String s) {\n            return URLDecoder.decode(s);\n        }\n    };\n\n    public static Multimap parseUrlEncoded(String query) {\n        return parse(query, \"&\", false, URL_DECODER);\n    }\n\n    @Override\n    public Iterator<NameValuePair> iterator() {\n        ArrayList<NameValuePair> ret = new ArrayList<NameValuePair>();\n        for (String name: keySet()) {\n            List<String> values = get(name);\n            for (String value: values) {\n                ret.add(new BasicNameValuePair(name, value));\n            }\n        }\n        return ret.iterator();\n    }\n\n    public Map<String, String> toSingleMap() {\n        HashMap<String, String> ret = new HashMap<>();\n        for (String key: keySet()) {\n            ret.put(key, getString(key));\n        }\n        return ret;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/NameValuePair.java",
    "content": "/*\n * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/NameValuePair.java $\n * $Revision: 496070 $\n * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $\n *\n * ====================================================================\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  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,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n * ====================================================================\n *\n * This software consists of voluntary contributions made by many\n * individuals on behalf of the Apache Software Foundation.  For more\n * information on the Apache Software Foundation, please see\n * <http://www.apache.org/>.\n *\n */\n\npackage com.jeffmony.async.http;\n\n/**\n * A simple class encapsulating an attribute/value pair.\n * <p>\n *  This class comforms to the generic grammar and formatting rules outlined in the \n *  <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2\">Section 2.2</a>\n *  and  \n *  <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6\">Section 3.6</a>\n *  of <a href=\"http://www.w3.org/Protocols/rfc2616/rfc2616.txt\">RFC 2616</a>\n * </p>\n * <h>2.2 Basic Rules</h>\n * <p>\n *  The following rules are used throughout this specification to describe basic parsing constructs. \n *  The US-ASCII coded character set is defined by ANSI X3.4-1986.\n * </p>\n * <pre>\n *     OCTET          = <any 8-bit sequence of data>\n *     CHAR           = <any US-ASCII character (octets 0 - 127)>\n *     UPALPHA        = <any US-ASCII uppercase letter \"A\"..\"Z\">\n *     LOALPHA        = <any US-ASCII lowercase letter \"a\"..\"z\">\n *     ALPHA          = UPALPHA | LOALPHA\n *     DIGIT          = <any US-ASCII digit \"0\"..\"9\">\n *     CTL            = <any US-ASCII control character\n *                      (octets 0 - 31) and DEL (127)>\n *     CR             = <US-ASCII CR, carriage return (13)>\n *     LF             = <US-ASCII LF, linefeed (10)>\n *     SP             = <US-ASCII SP, space (32)>\n *     HT             = <US-ASCII HT, horizontal-tab (9)>\n *     <\">            = <US-ASCII double-quote mark (34)>\n * </pre>\n * <p>\n *  Many HTTP/1.1 header field values consist of words separated by LWS or special \n *  characters. These special characters MUST be in a quoted string to be used within \n *  a parameter value (as defined in section 3.6).\n * <p>\n * <pre>\n * token          = 1*<any CHAR except CTLs or separators>\n * separators     = \"(\" | \")\" | \"<\" | \">\" | \"@\"\n *                | \",\" | \";\" | \":\" | \"\\\" | <\">\n *                | \"/\" | \"[\" | \"]\" | \"?\" | \"=\"\n *                | \"{\" | \"}\" | SP | HT\n * </pre>\n * <p>\n *  A string of text is parsed as a single word if it is quoted using double-quote marks.\n * </p>\n * <pre>\n * quoted-string  = ( <\"> *(qdtext | quoted-pair ) <\"> )\n * qdtext         = <any TEXT except <\">>\n * </pre>\n * <p>\n *  The backslash character (\"\\\") MAY be used as a single-character quoting mechanism only \n *  within quoted-string and comment constructs.\n * </p>\n * <pre>\n * quoted-pair    = \"\\\" CHAR\n * </pre>\n * <h>3.6 Transfer Codings</h>\n * <p>\n *  Parameters are in the form of attribute/value pairs.\n * </p>\n * <pre>\n * parameter               = attribute \"=\" value\n * attribute               = token\n * value                   = token | quoted-string\n * </pre> \n *\n * @author <a href=\"mailto:oleg at ural.com\">Oleg Kalnichevski</a>\n *\n */\npublic interface NameValuePair {\n\n    String getName();\n\n    String getValue();\n\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/Protocol.java",
    "content": "package com.jeffmony.async.http;\n\nimport java.util.Hashtable;\nimport java.util.Locale;\n\n/**\n * Protocols that OkHttp implements for <a\n * href=\"http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04\">NPN</a> and\n * <a href=\"http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg\">ALPN</a>\n * selection.\n * <p/>\n * <h3>Protocol vs Scheme</h3>\n * Despite its name, {@link java.net.URL#getProtocol()} returns the\n * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not\n * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word <i>protocol</i>\n * to identify how HTTP messages are framed.\n */\npublic enum Protocol {\n    /**\n     * An obsolete plaintext framing that does not use persistent sockets by\n     * default.\n     */\n    HTTP_1_0(\"http/1.0\"),\n\n    /**\n     * A plaintext framing that includes persistent connections.\n     * <p/>\n     * <p>This version of OkHttp implements <a\n     * href=\"http://www.ietf.org/rfc/rfc2616.txt\">RFC 2616</a>, and tracks\n     * revisions to that spec.\n     */\n    HTTP_1_1(\"http/1.1\"),\n\n    /**\n     * Chromium's binary-framed protocol that includes header compression,\n     * multiplexing multiple requests on the same socket, and server-push.\n     * HTTP/1.1 semantics are layered on SPDY/3.\n     * <p/>\n     * <p>This version of OkHttp implements SPDY 3 <a\n     * href=\"http://dev.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1\">draft\n     * 3.1</a>. Future releases of OkHttp may use this identifier for a newer draft\n     * of the SPDY spec.\n     */\n    SPDY_3(\"spdy/3.1\") {\n        @Override\n        public boolean needsSpdyConnection() {\n            return true;\n        }\n    },\n\n    /**\n     * The IETF's binary-framed protocol that includes header compression,\n     * multiplexing multiple requests on the same socket, and server-push.\n     * HTTP/1.1 semantics are layered on HTTP/2.\n     * <p/>\n     * <p>This version of OkHttp implements HTTP/2 <a\n     * href=\"http://tools.ietf.org/html/draft-ietf-httpbis-http2-13\">draft 12</a>\n     * with HPACK <a\n     * href=\"http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08\">draft\n     * 6</a>. Future releases of OkHttp may use this identifier for a newer draft\n     * of these specs.\n     */\n    HTTP_2(\"h2-13\") {\n        @Override\n        public boolean needsSpdyConnection() {\n            return true;\n        }\n    };\n\n    private final String protocol;\n    private static final Hashtable<String, Protocol> protocols = new Hashtable<String, Protocol>();\n\n    static {\n        protocols.put(HTTP_1_0.toString(), HTTP_1_0);\n        protocols.put(HTTP_1_1.toString(), HTTP_1_1);\n        protocols.put(SPDY_3.toString(), SPDY_3);\n        protocols.put(HTTP_2.toString(), HTTP_2);\n    }\n\n\n    Protocol(String protocol) {\n        this.protocol = protocol;\n    }\n\n    /**\n     * Returns the protocol identified by {@code protocol}.\n     */\n    public static Protocol get(String protocol) {\n        if (protocol == null)\n            return null;\n        return protocols.get(protocol.toLowerCase(Locale.US));\n    }\n\n    /**\n     * Returns the string used to identify this protocol for ALPN and NPN, like\n     * \"http/1.1\", \"spdy/3.1\" or \"h2-13\".\n     */\n    @Override\n    public String toString() {\n        return protocol;\n    }\n\n    public boolean needsSpdyConnection() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/ProtocolVersion.java",
    "content": "/*\n * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/ProtocolVersion.java $\n * $Revision: 609106 $\n * $Date: 2008-01-05 01:15:42 -0800 (Sat, 05 Jan 2008) $\n *\n * ====================================================================\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  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,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n * ====================================================================\n *\n * This software consists of voluntary contributions made by many\n * individuals on behalf of the Apache Software Foundation.  For more\n * information on the Apache Software Foundation, please see\n * <http://www.apache.org/>.\n *\n */\n\npackage com.jeffmony.async.http;\n\nimport java.io.Serializable;\n\n\n/**\n * Represents a protocol version, as specified in RFC 2616.\n * RFC 2616 specifies only HTTP versions, like \"HTTP/1.1\" and \"HTTP/1.0\".\n * RFC 3261 specifies a message format that is identical to HTTP except\n * for the protocol name. It defines a protocol version \"SIP/2.0\".\n * There are some nitty-gritty differences between the interpretation\n * of versions in HTTP and SIP. In those cases, HTTP takes precedence.\n * <p>\n * This class defines a protocol version as a combination of\n * protocol name, major version number, and minor version number.\n * Note that {@link #equals} and {@link #hashCode} are defined as\n * final here, they cannot be overridden in derived classes.\n * \n * @author <a href=\"mailto:oleg@ural.ru\">Oleg Kalnichevski</a>\n * @author <a href=\"mailto:rolandw at apache.org\">Roland Weber</a>\n * \n * @version $Revision: 609106 $\n */\npublic class ProtocolVersion implements Serializable, Cloneable {\n\n    private static final long serialVersionUID = 8950662842175091068L;\n\n\n    /** Name of the protocol. */\n    protected final String protocol;\n\n    /** Major version number of the protocol */\n    protected final int major;\n\n    /** Minor version number of the protocol */\n    protected final int minor;\n\n    \n    /**\n     * Create a protocol version designator.\n     *\n     * @param protocol   the name of the protocol, for example \"HTTP\"\n     * @param major      the major version number of the protocol\n     * @param minor      the minor version number of the protocol\n     */\n    public ProtocolVersion(String protocol, int major, int minor) {\n        if (protocol == null) {\n            throw new IllegalArgumentException\n                (\"Protocol name must not be null.\");\n        }\n        if (major < 0) {\n            throw new IllegalArgumentException\n                (\"Protocol major version number must not be negative.\");\n        }\n        if (minor < 0) {\n            throw new IllegalArgumentException\n                (\"Protocol minor version number may not be negative\");\n        }\n        this.protocol = protocol;\n        this.major = major;\n        this.minor = minor;\n    }\n\n    /**\n     * Returns the name of the protocol.\n     * \n     * @return the protocol name\n     */\n    public final String getProtocol() {\n        return protocol;\n    }\n\n    /**\n     * Returns the major version number of the protocol.\n     * \n     * @return the major version number.\n     */\n    public final int getMajor() {\n        return major;\n    }\n\n    /**\n     * Returns the minor version number of the HTTP protocol.\n     * \n     * @return the minor version number.\n     */\n    public final int getMinor() {\n        return minor;\n    }\n\n\n    /**\n     * Obtains a specific version of this protocol.\n     * This can be used by derived classes to instantiate themselves instead\n     * of the base class, and to define constants for commonly used versions.\n     * <br/>\n     * The default implementation in this class returns <code>this</code>\n     * if the version matches, and creates a new {@link ProtocolVersion}\n     * otherwise.\n     *\n     * @param major     the major version\n     * @param minor     the minor version\n     *\n     * @return  a protocol version with the same protocol name\n     *          and the argument version\n     */\n    public ProtocolVersion forVersion(int major, int minor) {\n\n        if ((major == this.major) && (minor == this.minor)) {\n            return this;\n        }\n\n        // argument checking is done in the constructor\n        return new ProtocolVersion(this.protocol, major, minor);\n    }\n\n\n    /**\n     * Obtains a hash code consistent with {@link #equals}.\n     *\n     * @return  the hashcode of this protocol version\n     */\n    public final int hashCode() {\n        return this.protocol.hashCode() ^ (this.major * 100000) ^ this.minor;\n    }\n\n        \n    /**\n     * Checks equality of this protocol version with an object.\n     * The object is equal if it is a protocl version with the same\n     * protocol name, major version number, and minor version number.\n     * The specific class of the object is <i>not</i> relevant,\n     * instances of derived classes with identical attributes are\n     * equal to instances of the base class and vice versa.\n     *\n     * @param obj       the object to compare with\n     *\n     * @return  <code>true</code> if the argument is the same protocol version,\n     *          <code>false</code> otherwise\n     */\n    public final boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (!(obj instanceof ProtocolVersion)) {\n            return false;\n        }\n        ProtocolVersion that = (ProtocolVersion) obj;\n\n        return ((this.protocol.equals(that.protocol)) &&\n                (this.major == that.major) &&\n                (this.minor == that.minor));\n    }\n\n\n    /**\n     * Checks whether this protocol can be compared to another one.\n     * Only protocol versions with the same protocol name can be\n     * {@link #compareToVersion compared}.\n     *\n     * @param that      the protocol version to consider\n     *\n     * @return  <code>true</code> if {@link #compareToVersion compareToVersion}\n     *          can be called with the argument, <code>false</code> otherwise\n     */\n    public boolean isComparable(ProtocolVersion that) {\n        return (that != null) && this.protocol.equals(that.protocol);\n    }\n\n\n    /**\n     * Compares this protocol version with another one.\n     * Only protocol versions with the same protocol name can be compared.\n     * This method does <i>not</i> define a total ordering, as it would be\n     * required for {@link java.lang.Comparable}.\n     *\n     * @param that      the protocl version to compare with\n     *  \n     * @return   a negative integer, zero, or a positive integer\n     *           as this version is less than, equal to, or greater than\n     *           the argument version.\n     *\n     * @throws IllegalArgumentException\n     *         if the argument has a different protocol name than this object,\n     *         or if the argument is <code>null</code>\n     */\n    public int compareToVersion(ProtocolVersion that) {\n        if (that == null) {\n            throw new IllegalArgumentException\n                (\"Protocol version must not be null.\"); \n        }\n        if (!this.protocol.equals(that.protocol)) {\n            throw new IllegalArgumentException\n                (\"Versions for different protocols cannot be compared. \" +\n                 this + \" \" + that);\n        }\n\n        int delta = getMajor() - that.getMajor();\n        if (delta == 0) {\n            delta = getMinor() - that.getMinor();\n        }\n        return delta;\n    }\n\n\n    /**\n     * Tests if this protocol version is greater or equal to the given one.\n     *\n     * @param version   the version against which to check this version\n     *\n     * @return  <code>true</code> if this protocol version is\n     *          {@link #isComparable comparable} to the argument\n     *          and {@link #compareToVersion compares} as greater or equal,\n     *          <code>false</code> otherwise\n     */\n    public final boolean greaterEquals(ProtocolVersion version) {\n        return isComparable(version) && (compareToVersion(version) >= 0);\n    }\n\n\n    /**\n     * Tests if this protocol version is less or equal to the given one.\n     *\n     * @param version   the version against which to check this version\n     *\n     * @return  <code>true</code> if this protocol version is\n     *          {@link #isComparable comparable} to the argument\n     *          and {@link #compareToVersion compares} as less or equal,\n     *          <code>false</code> otherwise\n     */\n    public final boolean lessEquals(ProtocolVersion version) {\n        return isComparable(version) && (compareToVersion(version) <= 0);\n    }\n\n\n    /**\n     * Converts this protocol version to a string.\n     *\n     * @return  a protocol version string, like \"HTTP/1.1\"\n     */\n    public String toString() {\n        StringBuilder buffer = new StringBuilder();\n        buffer.append(this.protocol); \n        buffer.append('/'); \n        buffer.append(Integer.toString(this.major));\n        buffer.append('.'); \n        buffer.append(Integer.toString(this.minor));\n        return buffer.toString();\n    }\n\n    public Object clone() throws CloneNotSupportedException {\n        return super.clone();\n    }\n    \n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/RedirectLimitExceededException.java",
    "content": "package com.jeffmony.async.http;\n\npublic class RedirectLimitExceededException extends Exception {\n    public RedirectLimitExceededException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/RequestLine.java",
    "content": "/*\n * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/RequestLine.java $\n * $Revision: 573864 $\n * $Date: 2007-09-08 08:53:25 -0700 (Sat, 08 Sep 2007) $\n *\n * ====================================================================\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  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,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n * ====================================================================\n *\n * This software consists of voluntary contributions made by many\n * individuals on behalf of the Apache Software Foundation.  For more\n * information on the Apache Software Foundation, please see\n * <http://www.apache.org/>.\n *\n */\n\npackage com.jeffmony.async.http;\n\n/**\n * The first line of an\n * It contains the method, URI, and HTTP version of the request.\n * For details, see RFC 2616.\n *\n * @author <a href=\"mailto:oleg at ural.ru\">Oleg Kalnichevski</a>\n *\n * @version $Revision: 573864 $\n * \n * @since 4.0\n */\npublic interface RequestLine {\n\n    String getMethod();\n\n    ProtocolVersion getProtocolVersion();\n\n    String getUri();\n    \n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/SSLEngineSNIConfigurator.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.os.Build;\n\nimport java.lang.reflect.Field;\nimport java.util.Hashtable;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLEngine;\n\n/**\n * Created by koush on 12/8/14.\n */\npublic class SSLEngineSNIConfigurator implements AsyncSSLEngineConfigurator {\n    private static class EngineHolder implements AsyncSSLEngineConfigurator {\n        Field peerHost;\n        Field peerPort;\n        Field sslParameters;\n        Field useSni;\n        boolean skipReflection;\n\n        @Override\n        public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) {\n            return null;\n        }\n\n        public EngineHolder(Class engineClass) {\n            try {\n                peerHost = engineClass.getSuperclass().getDeclaredField(\"peerHost\");\n                peerHost.setAccessible(true);\n\n                peerPort = engineClass.getSuperclass().getDeclaredField(\"peerPort\");\n                peerPort.setAccessible(true);\n\n                sslParameters = engineClass.getDeclaredField(\"sslParameters\");\n                sslParameters.setAccessible(true);\n\n                useSni = sslParameters.getType().getDeclaredField(\"useSni\");\n                useSni.setAccessible(true);\n            }\n            catch (NoSuchFieldException e) {\n            }\n        }\n\n        @Override\n        public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) {\n            if (useSni == null || skipReflection)\n                return;\n            try {\n                peerHost.set(engine, host);\n                peerPort.set(engine, port);\n                Object sslp = sslParameters.get(engine);\n                useSni.set(sslp, true);\n            }\n            catch (IllegalAccessException e) {\n            }\n        }\n    }\n\n    Hashtable<String, EngineHolder> holders = new Hashtable<String, EngineHolder>();\n\n    @Override\n    public SSLEngine createEngine(SSLContext sslContext, String peerHost, int peerPort) {\n        // pre M, must use reflection to enable SNI, otherwise createSSLEngine(peerHost, peerPort) works.\n        SSLEngine engine;\n        boolean skipReflection = \"GmsCore_OpenSSL\".equals(sslContext.getProvider().getName()) || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;\n        if (skipReflection)\n            engine = sslContext.createSSLEngine(peerHost, peerPort);\n        else\n            engine = sslContext.createSSLEngine();\n//        ensureHolder(engine).skipReflection = skipReflection;\n        return engine;\n    }\n\n    EngineHolder ensureHolder(SSLEngine engine) {\n        String name = engine.getClass().getCanonicalName();\n        EngineHolder holder = holders.get(name);\n        if (holder == null) {\n            holder = new EngineHolder(engine.getClass());\n            holders.put(name, holder);\n        }\n        return holder;\n    }\n\n    @Override\n    public void configureEngine(SSLEngine engine, AsyncHttpClientMiddleware.GetSocketData data, String host, int port) {\n        EngineHolder holder = ensureHolder(engine);\n        holder.configureEngine(engine, data, host, port);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/SimpleMiddleware.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.future.Cancellable;\n\npublic class SimpleMiddleware implements AsyncHttpClientMiddleware {\n    @Override\n    public void onRequest(OnRequestData data) {\n    }\n\n    @Override\n    public Cancellable getSocket(GetSocketData data) {\n        return null;\n    }\n\n    @Override\n    public boolean exchangeHeaders(OnExchangeHeaderData data) {\n        return false;\n    }\n\n    @Override\n    public void onRequestSent(OnRequestSentData data) {\n    }\n\n    @Override\n    public void onHeadersReceived(OnHeadersReceivedData data) {\n    }\n\n    @Override\n    public void onBodyDecoder(OnBodyDecoderData data) {\n    }\n\n    @Override\n    public AsyncHttpRequest onResponseReady(OnResponseReadyData data) {\n        return null;\n    }\n\n    @Override\n    public void onResponseComplete(OnResponseCompleteData data) {\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/TaggedList.java",
    "content": "package com.jeffmony.async.http;\n\nimport java.util.ArrayList;\n\npublic class TaggedList<T> extends ArrayList<T> {\n    private Object tag;\n\n    public synchronized <V> V tag() {\n        return (V)tag;\n    }\n\n    public synchronized <V> void tag(V tag) {\n        this.tag = tag;\n    }\n\n    public synchronized <V> void tagNull(V tag) {\n        if (this.tag == null)\n            this.tag = tag;\n    }\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/WebSocket.java",
    "content": "package com.jeffmony.async.http;\n\nimport com.jeffmony.async.AsyncSocket;\n\npublic interface WebSocket extends AsyncSocket {\n    interface StringCallback {\n        void onStringAvailable(String s);\n    }\n    interface PingCallback {\n        void onPingReceived(String s);\n    }\n    interface PongCallback {\n        void onPongReceived(String s);\n    }\n\n    void send(byte[] bytes);\n    void send(String string);\n    void send(byte[] bytes, int offset, int len);\n    void ping(String message);\n    void pong(String message);\n    \n    void setStringCallback(StringCallback callback);\n    StringCallback getStringCallback();\n\n    void setPingCallback(PingCallback callback);\n    \n    void setPongCallback(PongCallback callback);\n    PongCallback getPongCallback();\n\n    boolean isBuffering();\n    String getProtocol();\n    \n    AsyncSocket getSocket();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/WebSocketHandshakeException.java",
    "content": "package com.jeffmony.async.http;\n\npublic class WebSocketHandshakeException extends Exception {\n    public WebSocketHandshakeException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/WebSocketImpl.java",
    "content": "package com.jeffmony.async.http;\n\nimport android.text.TextUtils;\nimport android.util.Base64;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.BufferedDataSink;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.http.server.AsyncHttpServerRequest;\nimport com.jeffmony.async.http.server.AsyncHttpServerResponse;\n\nimport java.nio.ByteBuffer;\nimport java.nio.LongBuffer;\nimport java.security.MessageDigest;\nimport java.util.LinkedList;\nimport java.util.UUID;\n\npublic class WebSocketImpl implements WebSocket {\n    @Override\n    public void end() {\n        mSocket.end();\n    }\n    \n    private static byte[] toByteArray(UUID uuid) {\n    \tbyte[] byteArray = new byte[(Long.SIZE / Byte.SIZE) * 2];\n    \tByteBuffer buffer = ByteBuffer.wrap(byteArray);\n    \tLongBuffer longBuffer = buffer.asLongBuffer();\n    \tlongBuffer.put(new long[] { uuid.getMostSignificantBits(),uuid.getLeastSignificantBits() });\n    \treturn byteArray;\n    }\n\n    private static String SHA1(String text) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-1\");\n            md.update(text.getBytes(\"iso-8859-1\"), 0, text.length());\n            byte[] sha1hash = md.digest();\n            return Base64.encodeToString(sha1hash, Base64.NO_WRAP);\n        }\n        catch (Exception ex) {\n            return null;\n        }\n    }\n\n    final static String MAGIC = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n    \n    private LinkedList<ByteBufferList> pending;\n\n    private void addAndEmit(ByteBufferList bb) {\n        if (pending == null) {\n            Util.emitAllData(this, bb);\n            if (bb.remaining() > 0) {\n                pending = new LinkedList<ByteBufferList>();\n                pending.add(bb);\n            }\n            return;\n        }\n        \n        while (!isPaused()) {\n            bb = pending.remove();\n            Util.emitAllData(this, bb);\n            if (bb.remaining() > 0)\n                pending.add(0, bb);\n        }\n        if (pending.size() == 0)\n            pending = null;\n    }\n\n    private void setupParser(boolean masking, boolean deflate) {\n        mParser = new HybiParser(mSocket) {\n            @Override\n            protected void report(Exception ex) {\n                if (WebSocketImpl.this.mExceptionCallback != null)\n                    WebSocketImpl.this.mExceptionCallback.onCompleted(ex);\n            }\n            @Override\n            protected void onMessage(byte[] payload) {\n                addAndEmit(new ByteBufferList(payload));\n            }\n\n            @Override\n            protected void onMessage(String payload) {\n                if (WebSocketImpl.this.mStringCallback != null)\n                    WebSocketImpl.this.mStringCallback.onStringAvailable(payload);\n            }\n            @Override\n            protected void onDisconnect(int code, String reason) {\n                mSocket.close();\n//                if (WebSocketImpl.this.mClosedCallback != null)\n//                    WebSocketImpl.this.mClosedCallback.onCompleted(null);\n            }\n            @Override\n            protected void sendFrame(byte[] frame) {\n                mSink.write(new ByteBufferList(frame));\n            }\n\n            @Override\n            protected void onPing(String payload) {\n                if (WebSocketImpl.this.mPingCallback != null)\n                    WebSocketImpl.this.mPingCallback.onPingReceived(payload);\n            }\n\n            @Override\n            protected void onPong(String payload) {\n                if (WebSocketImpl.this.mPongCallback != null)\n                    WebSocketImpl.this.mPongCallback.onPongReceived(payload);\n            }\n        };\n        mParser.setMasking(masking);\n        mParser.setDeflate(deflate);\n        if (mSocket.isPaused())\n            mSocket.resume();\n    }\n    \n    private AsyncSocket mSocket;\n    BufferedDataSink mSink;\n    public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n        this(request.getSocket());\n        \n        String key = request.getHeaders().get(\"Sec-WebSocket-Key\");\n        String concat = key + MAGIC;\n        String sha1 = SHA1(concat);\n        String origin = request.getHeaders().get(\"Origin\");\n        \n        response.code(101);\n        response.getHeaders().set(\"Upgrade\", \"WebSocket\");\n        response.getHeaders().set(\"Connection\", \"Upgrade\");\n        response.getHeaders().set(\"Sec-WebSocket-Accept\", sha1);\n        String protocol = request.getHeaders().get(\"Sec-WebSocket-Protocol\");\n        // match the protocol (sanity checking and enforcement is done in the caller)\n        if (!TextUtils.isEmpty(protocol))\n            response.getHeaders().set(\"Sec-WebSocket-Protocol\", protocol);\n//        if (origin != null)\n//            response.getHeaders().getHeaders().set(\"Access-Control-Allow-Origin\", \"http://\" + origin);\n        response.writeHead();\n        \n        setupParser(false, false);\n    }\n\n    String protocol;\n    @Override\n    public String getProtocol() {\n        return protocol;\n    }\n\n    public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String... protocols) {\n        Headers headers = req.getHeaders();\n        final String key = Base64.encodeToString(toByteArray(UUID.randomUUID()), Base64.NO_WRAP);\n        headers.set(\"Sec-WebSocket-Version\", \"13\");\n        headers.set(\"Sec-WebSocket-Key\", key);\n        headers.set(\"Sec-WebSocket-Extensions\", \"x-webkit-deflate-frame\");\n        headers.set(\"Connection\", \"Upgrade\");\n        headers.set(\"Upgrade\", \"websocket\");\n        if (protocols != null) {\n            for (String protocol: protocols) {\n                headers.add(\"Sec-WebSocket-Protocol\", protocol);\n            }\n        }\n        headers.set(\"Pragma\", \"no-cache\");\n        headers.set(\"Cache-Control\", \"no-cache\");\n        if (TextUtils.isEmpty(req.getHeaders().get(\"User-Agent\")))\n            req.getHeaders().set(\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36\");\n    }\n    \n    public WebSocketImpl(AsyncSocket socket) {\n        mSocket = socket;\n        mSink = new BufferedDataSink(mSocket);\n    }\n    \n    public static WebSocket finishHandshake(Headers requestHeaders, AsyncHttpResponse response) {\n        if (response == null)\n            return null;\n        if (response.code() != 101)\n            return null;\n        if (!\"websocket\".equalsIgnoreCase(response.headers().get(\"Upgrade\")))\n            return null;\n        \n        String sha1 = response.headers().get(\"Sec-WebSocket-Accept\");\n        if (sha1 == null)\n            return null;\n        String key = requestHeaders.get(\"Sec-WebSocket-Key\");\n        if (key == null)\n            return null;\n        String concat = key + MAGIC;\n        String expected = SHA1(concat).trim();\n        if (!sha1.equalsIgnoreCase(expected))\n            return null;\n        String extensions = requestHeaders.get(\"Sec-WebSocket-Extensions\");\n        boolean deflate = false;\n        if (extensions != null) {\n            if (extensions.equals(\"x-webkit-deflate-frame\"))\n                deflate = true;\n            // is this right? do we want to crap out here? Commenting out\n            // as I suspect this caused a regression.\n//            else\n//                return null;\n        }\n\n        WebSocketImpl ret = new WebSocketImpl(response.detachSocket());\n        ret.protocol = response.headers().get(\"Sec-WebSocket-Protocol\");\n        ret.setupParser(true, deflate);\n        return ret;\n    }\n    \n    HybiParser mParser;\n\n    @Override\n    public void close() {\n        mSocket.close();\n    }\n\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        mSocket.setClosedCallback(handler);\n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        return mSocket.getClosedCallback();\n    }\n\n    CompletedCallback mExceptionCallback;\n    @Override\n    public void setEndCallback(CompletedCallback callback) {\n        mExceptionCallback = callback;\n    }\n\n    @Override\n    public CompletedCallback getEndCallback() {\n        return mExceptionCallback;\n    }\n\n    @Override\n    public void send(byte[] bytes) {\n        getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(bytes)))));\n    }\n    \n    @Override\n    public void send(byte[] bytes, int offset, int len) {\n        getServer().post(() -> mSink.write(new ByteBufferList(mParser.frame(bytes, offset, len))));\n    }\n\n    @Override\n    public void send(String string) {\n        getServer().post(() -> mSink.write(new ByteBufferList((mParser.frame(string)))));\n    }\n\n    @Override\n    public void ping(String string) {\n        getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pingFrame(string)))));\n    }\n\n    @Override\n    public void pong(String string) {\n        getServer().post(() -> mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pongFrame(string)))));\n    }\n\n    private StringCallback mStringCallback;\n    @Override\n    public void setStringCallback(StringCallback callback) {\n        mStringCallback = callback;\n    }\n\n    private DataCallback mDataCallback;\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        mDataCallback = callback;\n    }\n\n    @Override\n    public StringCallback getStringCallback() {\n        return mStringCallback;\n    }\n\n    private PingCallback mPingCallback;\n    @Override\n    public void setPingCallback(PingCallback callback) {\n        mPingCallback = callback;\n    }\n\n    private PongCallback mPongCallback;\n    @Override\n    public void setPongCallback(PongCallback callback) {\n        mPongCallback = callback;\n    }\n\n    @Override\n    public PongCallback getPongCallback() {\n        return mPongCallback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return mDataCallback;\n    }\n\n    @Override\n    public boolean isOpen() {\n        return mSocket.isOpen();\n    }\n    \n    @Override\n    public boolean isBuffering() {\n        return mSink.remaining() > 0;\n    }\n\n    @Override\n    public void write(ByteBufferList bb) {\n        byte[] buf = bb.getAllByteArray();\n        send(buf);\n    }\n\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        mSink.setWriteableCallback(handler);\n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        return mSink.getWriteableCallback();\n    }\n    \n    @Override\n    public AsyncSocket getSocket() {\n        return mSocket;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mSocket.getServer();\n    }\n\n    @Override\n    public boolean isChunked() {\n        return false;\n    }\n\n    @Override\n    public void pause() {\n        mSocket.pause();\n    }\n\n    @Override\n    public void resume() {\n        mSocket.resume();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return mSocket.isPaused();\n    }\n\n    @Override\n    public String charset() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/AsyncHttpRequestBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\n\npublic interface AsyncHttpRequestBody<T> {\n    void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed);\n    void parse(DataEmitter emitter, CompletedCallback completed);\n    String getContentType();\n    boolean readFullyOnRequest();\n    int length();\n    T get();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/ByteBufferListRequestBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.parser.ByteBufferListParser;\n\npublic class ByteBufferListRequestBody implements AsyncHttpRequestBody<ByteBufferList> {\n    public ByteBufferListRequestBody() {\n    }\n\n    ByteBufferList bb;\n    public ByteBufferListRequestBody(ByteBufferList bb) {\n        this.bb = bb;\n    }\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) {\n        Util.writeAll(sink, bb, completed);\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, CompletedCallback completed) {\n        new ByteBufferListParser().parse(emitter).setCallback((e, result) -> {\n            bb = result;\n            completed.onCompleted(e);\n        });\n    }\n\n    public static String CONTENT_TYPE = \"application/binary\";\n\n    @Override\n    public String getContentType() {\n        return CONTENT_TYPE;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        return bb.remaining();\n    }\n\n    @Override\n    public ByteBufferList get() {\n        return bb;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/DocumentBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.parser.DocumentParser;\nimport com.jeffmony.async.util.Charsets;\n\nimport org.w3c.dom.Document;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.OutputStreamWriter;\n\nimport javax.xml.transform.Transformer;\nimport javax.xml.transform.TransformerFactory;\nimport javax.xml.transform.dom.DOMSource;\nimport javax.xml.transform.stream.StreamResult;\n\n/**\n * Created by koush on 8/30/13.\n */\npublic class DocumentBody implements AsyncHttpRequestBody<Document> {\n    public DocumentBody() {\n        this(null);\n    }\n\n    public DocumentBody(Document document) {\n        this.document = document;\n    }\n\n    ByteArrayOutputStream bout;\n    private void prepare() {\n        if (bout != null)\n            return;\n\n        try {\n            DOMSource source = new DOMSource(document);\n            TransformerFactory tf = TransformerFactory.newInstance();\n            Transformer transformer = tf.newTransformer();\n            bout = new ByteArrayOutputStream();\n            OutputStreamWriter writer = new OutputStreamWriter(bout, Charsets.UTF_8);\n            StreamResult result = new StreamResult(writer);\n            transformer.transform(source, result);\n            writer.flush();\n        }\n        catch (Exception e) {\n        }\n    }\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) {\n        prepare();\n        byte[] bytes = bout.toByteArray();\n        Util.writeAll(sink, bytes, completed);\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        new DocumentParser().parse(emitter).setCallback(new FutureCallback<Document>() {\n            @Override\n            public void onCompleted(Exception e, Document result) {\n                document = result;\n                completed.onCompleted(e);\n            }\n        });\n    }\n\n    public static final String CONTENT_TYPE = \"application/xml\";\n\n    @Override\n    public String getContentType() {\n        return CONTENT_TYPE;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        prepare();\n        return bout.size();\n    }\n\n    Document document;\n    @Override\n    public Document get() {\n        return document;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/FileBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\n\nimport java.io.File;\n\n/**\n * Created by koush on 10/14/13.\n */\npublic class FileBody implements AsyncHttpRequestBody<File> {\n    public static final String CONTENT_TYPE = \"application/binary\";\n\n    File file;\n    String contentType = CONTENT_TYPE;\n\n    public FileBody(File file) {\n        this.file = file;\n    }\n\n    public FileBody(File file, String contentType) {\n        this.file = file;\n        this.contentType = contentType;\n    }\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) {\n        Util.pump(file, sink, completed);\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, CompletedCallback completed) {\n        throw new AssertionError(\"not implemented\");\n    }\n\n    @Override\n    public String getContentType() {\n        return contentType;\n    }\n\n    public void setContentType(String contentType) {\n        this.contentType = contentType;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        throw new AssertionError(\"not implemented\");\n    }\n\n    @Override\n    public int length() {\n        return (int)file.length();\n    }\n\n    @Override\n    public File get() {\n        return file;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/FilePart.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.http.BasicNameValuePair;\nimport com.jeffmony.async.http.NameValuePair;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\n\npublic class FilePart extends StreamPart {\n    File file;\n    public FilePart(String name, final File file) {\n        super(name, (int)file.length(), new ArrayList<NameValuePair>() {\n            {\n                add(new BasicNameValuePair(\"filename\", file.getName()));\n            }\n        });\n\n//        getRawHeaders().set(\"Content-Type\", \"application/xml\");\n\n        this.file = file;\n    }\n\n    @Override\n    protected InputStream getInputStream() throws IOException {\n        return new FileInputStream(file);\n    }\n\n    @Override\n    public String toString() {\n        return getName();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/JSONArrayBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.parser.JSONArrayParser;\n\nimport org.json.JSONArray;\n\npublic class JSONArrayBody implements AsyncHttpRequestBody<JSONArray> {\n    public JSONArrayBody() {\n    }\n\n    byte[] mBodyBytes;\n    JSONArray json;\n    public JSONArrayBody(JSONArray json) {\n        this();\n        this.json = json;\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        new JSONArrayParser().parse(emitter).setCallback(new FutureCallback<JSONArray>() {\n            @Override\n            public void onCompleted(Exception e, JSONArray result) {\n                json = result;\n                completed.onCompleted(e);\n            }\n        });\n    }\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) {\n        Util.writeAll(sink, mBodyBytes, completed);\n    }\n\n    @Override\n    public String getContentType() {\n        return \"application/json\";\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        mBodyBytes = json.toString().getBytes();\n        return mBodyBytes.length;\n    }\n\n    public static final String CONTENT_TYPE = \"application/json\";\n\n    @Override\n    public JSONArray get() {\n        return json;\n    }\n}\n\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/JSONObjectBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.parser.JSONObjectParser;\n\nimport org.json.JSONObject;\n\npublic class JSONObjectBody implements AsyncHttpRequestBody<JSONObject> {\n    public JSONObjectBody() {\n    }\n    \n    byte[] mBodyBytes;\n    JSONObject json;\n    public JSONObjectBody(JSONObject json) {\n        this();\n        this.json = json;\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        new JSONObjectParser().parse(emitter).setCallback(new FutureCallback<JSONObject>() {\n            @Override\n            public void onCompleted(Exception e, JSONObject result) {\n                json = result;\n                completed.onCompleted(e);\n            }\n        });\n    }\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) {\n        Util.writeAll(sink, mBodyBytes, completed);\n    }\n\n    @Override\n    public String getContentType() {\n        return CONTENT_TYPE;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        mBodyBytes = json.toString().getBytes();\n        return mBodyBytes.length;\n    }\n\n    public static final String CONTENT_TYPE = \"application/json\";\n\n    @Override\n    public JSONObject get() {\n        return json;\n    }\n}\n\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/MultipartFormDataBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.LineEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ContinuationCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.future.Continuation;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.server.BoundaryEmitter;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\npublic class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpRequestBody<Multimap> {\n    LineEmitter liner;\n    Headers formData;\n    ByteBufferList lastData;\n    Part lastPart;\n\n    public interface MultipartCallback {\n        public void onPart(Part part);\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        setDataEmitter(emitter);\n        setEndCallback(completed);\n    }\n\n    void handleLast() {\n        if (lastData == null)\n            return;\n        \n        if (formData == null)\n            formData = new Headers();\n\n        String value = lastData.peekString();\n        String name = TextUtils.isEmpty(lastPart.getName()) ? \"unnamed\" : lastPart.getName();\n        StringPart part = new StringPart(name, value);\n        part.mHeaders = lastPart.mHeaders;\n        addPart(part);\n\n        formData.add(name, value);\n\n        lastPart = null;\n        lastData = null;\n    }\n    \n    public String getField(String name) {\n        if (formData == null)\n            return null;\n        return formData.get(name);\n    }\n    \n    @Override\n    protected void onBoundaryEnd() {\n        super.onBoundaryEnd();\n        handleLast();\n    }\n\n    @Override\n    protected void onBoundaryStart() {\n        final Headers headers = new Headers();\n        liner = new LineEmitter();\n        liner.setLineCallback(new LineEmitter.StringCallback() {\n            @Override\n            public void onStringAvailable(String s) {\n                if (!\"\\r\".equals(s)){\n                    headers.addLine(s);\n                }\n                else {\n                    handleLast();\n                    \n                    liner = null;\n                    setDataCallback(null);\n                    Part part = new Part(headers);\n                    if (mCallback != null)\n                        mCallback.onPart(part);\n                    if (getDataCallback() == null) {\n//                        if (part.isFile()) {\n//                            setDataCallback(new NullDataCallback());\n//                            return;\n//                        }\n\n                        lastPart = part;\n                        lastData = new ByteBufferList();\n                        setDataCallback(new DataCallback() {\n                            @Override\n                            public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                                bb.get(lastData);\n                            }\n                        });\n                    }\n                }\n            }\n        });\n        setDataCallback(liner);\n    }\n\n    public static final String PRIMARY_TYPE = \"multipart/\";\n    public static final String CONTENT_TYPE = PRIMARY_TYPE + \"form-data\";\n    String contentType = CONTENT_TYPE;\n    public MultipartFormDataBody(String contentType) {\n        Multimap map = Multimap.parseSemicolonDelimited(contentType);\n        String boundary = map.getString(\"boundary\");\n        if (boundary == null)\n            report(new Exception(\"No boundary found for multipart/form-data\"));\n        else\n            setBoundary(boundary);\n    }\n\n    MultipartCallback mCallback;\n    public void setMultipartCallback(MultipartCallback callback) {\n        mCallback = callback;\n    }\n    \n    public MultipartCallback getMultipartCallback() {\n        return mCallback;\n    }\n\n    int written;\n    @Override\n    public void write(AsyncHttpRequest request, final DataSink sink, final CompletedCallback completed) {\n        if (mParts == null)\n            return;\n\n        Continuation c = new Continuation(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                completed.onCompleted(ex);\n//                if (ex == null)\n//                    sink.end();\n//                else\n//                    sink.close();\n            }\n        });\n\n        for (final Part part: mParts) {\n            c.add(new ContinuationCallback() {\n                @Override\n                public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n                    byte[] bytes = part.getRawHeaders().toPrefixString(getBoundaryStart()).getBytes();\n                    Util.writeAll(sink, bytes, next);\n                    written += bytes.length;\n                }\n            })\n            .add(new ContinuationCallback() {\n                @Override\n                public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n                    long partLength = part.length();\n                    if (partLength >= 0)\n                        written += partLength;\n                    part.write(sink, next);\n                }\n            })\n            .add(new ContinuationCallback() {\n                @Override\n                public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n                    byte[] bytes = \"\\r\\n\".getBytes();\n                    Util.writeAll(sink, bytes, next);\n                    written += bytes.length;\n                }\n            });\n        }\n        c.add(new ContinuationCallback() {\n            @Override\n            public void onContinue(Continuation continuation, CompletedCallback next) throws Exception {\n                byte[] bytes = (getBoundaryEnd()).getBytes();\n                Util.writeAll(sink, bytes, next);\n                written += bytes.length;\n                \n                assert written == totalToWrite;\n            }\n        });\n        c.start();\n    }\n\n    @Override\n    public String getContentType() {\n        if (getBoundary() == null) {\n            setBoundary(\"----------------------------\" + UUID.randomUUID().toString().replace(\"-\", \"\"));\n        }\n        return contentType + \"; boundary=\" + getBoundary();\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return false;\n    }\n\n    int totalToWrite;\n    @Override\n    public int length() {\n        if (getBoundary() == null) {\n            setBoundary(\"----------------------------\" + UUID.randomUUID().toString().replace(\"-\", \"\"));\n        }\n\n        int length = 0;\n        for (final Part part: mParts) {\n            String partHeader = part.getRawHeaders().toPrefixString(getBoundaryStart());\n            if (part.length() == -1)\n                return -1;\n            length += part.length() + partHeader.getBytes().length + \"\\r\\n\".length();\n        }\n        length += (getBoundaryEnd()).getBytes().length;\n        return totalToWrite = length;\n    }\n    \n    public MultipartFormDataBody() {\n    }\n\n    public void setContentType(String contentType) {\n        this.contentType = contentType;\n    }\n\n    public List<Part> getParts() {\n        if (mParts == null)\n            return null;\n        return new ArrayList<>(mParts);\n    }\n\n    public void addFilePart(String name, File file) {\n        addPart(new FilePart(name, file));\n    }\n    \n    public void addStringPart(String name, String value) {\n        addPart(new StringPart(name, value));\n    }\n    \n    private ArrayList<Part> mParts;\n    public void addPart(Part part) {\n        if (mParts == null)\n            mParts = new ArrayList<Part>();\n        mParts.add(part);\n    }\n\n    @Override\n    public Multimap get() {\n        return new Multimap(formData.getMultiMap());\n    }\n\n    @Override\n    public String toString() {\n        for (Part part: getParts()) {\n            return part.toString();\n        }\n        return \"multipart content is empty\";\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/Part.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.NameValuePair;\n\nimport java.io.File;\nimport java.util.List;\nimport java.util.Locale;\n\npublic class Part {\n    public static final String CONTENT_DISPOSITION = \"Content-Disposition\";\n    \n    Headers mHeaders;\n    Multimap mContentDisposition;\n    public Part(Headers headers) {\n        mHeaders = headers;\n        mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION));\n    }\n    \n    public String getName() {\n        return mContentDisposition.getString(\"name\");\n    }\n    \n    private long length = -1;\n    public Part(String name, long length, List<NameValuePair> contentDisposition) {\n        this.length = length;\n        mHeaders = new Headers();\n        StringBuilder builder = new StringBuilder(String.format(Locale.ENGLISH, \"form-data; name=\\\"%s\\\"\", name));\n        if (contentDisposition != null) {\n            for (NameValuePair pair: contentDisposition) {\n                builder.append(String.format(Locale.ENGLISH, \"; %s=\\\"%s\\\"\", pair.getName(), pair.getValue()));\n            }\n        }\n        mHeaders.set(CONTENT_DISPOSITION, builder.toString());\n        mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION));\n    }\n\n    public Headers getRawHeaders() {\n        return mHeaders;\n    }\n\n    public String getContentType() {\n        return mHeaders.get(\"Content-Type\");\n    }\n\n    public void setContentType(String contentType) {\n        mHeaders.set(\"Content-Type\", contentType);\n    }\n\n    public String getFilename() {\n        String file = mContentDisposition.getString(\"filename\");\n        if (file == null)\n            return null;\n        return new File(file).getName();\n    }\n\n    public boolean isFile() {\n        return mContentDisposition.containsKey(\"filename\");\n    }\n    \n    public long length() {\n        return length;\n    }\n    \n    public void write(DataSink sink, CompletedCallback callback) {\n        assert false;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/StreamBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\n\nimport java.io.InputStream;\n\npublic class StreamBody implements AsyncHttpRequestBody<InputStream> {\n    InputStream stream;\n    int length;\n    String contentType = CONTENT_TYPE;\n\n    /**\n     * Construct an http body from a stream\n     * @param stream\n     * @param length Length of stream to read, or value < 0 to read to end\n     */\n    public StreamBody(InputStream stream, int length) {\n        this.stream = stream;\n        this.length = length;\n    }\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, CompletedCallback completed) {\n        Util.pump(stream, length < 0 ? Integer.MAX_VALUE : length, sink, completed);\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, CompletedCallback completed) {\n        throw new AssertionError(\"not implemented\");\n    }\n\n    public static final String CONTENT_TYPE = \"application/binary\";\n    @Override\n    public String getContentType() {\n        return contentType;\n    }\n    public StreamBody setContentType(String contentType) {\n        this.contentType = contentType;\n        return this;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        throw new AssertionError(\"not implemented\");\n    }\n\n    @Override\n    public int length() {\n        return length;\n    }\n\n    @Override\n    public InputStream get() {\n        return stream;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/StreamPart.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.NameValuePair;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\n\npublic abstract class StreamPart extends Part {\n    public StreamPart(String name, long length, List<NameValuePair> contentDisposition) {\n        super(name, length, contentDisposition);\n    }\n    \n    @Override\n    public void write(DataSink sink, CompletedCallback callback) {\n        try {\n            InputStream is = getInputStream();\n            Util.pump(is, sink, callback);\n        }\n        catch (Exception e) {\n            callback.onCompleted(e);\n        }\n    }\n    \n    protected abstract InputStream getInputStream() throws IOException;\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/StringBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.FutureCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.parser.StringParser;\n\npublic class StringBody implements AsyncHttpRequestBody<String> {\n    public StringBody() {\n    }\n\n    byte[] mBodyBytes;\n    String string;\n    public StringBody(String string) {\n        this();\n        this.string = string;\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        new StringParser().parse(emitter).setCallback(new FutureCallback<String>() {\n            @Override\n            public void onCompleted(Exception e, String result) {\n                string = result;\n                completed.onCompleted(e);\n            }\n        });\n    }\n\n    public static final String CONTENT_TYPE = \"text/plain\";\n\n    @Override\n    public void write(AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) {\n        if (mBodyBytes == null)\n            mBodyBytes = string.getBytes();\n        Util.writeAll(sink, mBodyBytes, completed);\n    }\n\n    @Override\n    public String getContentType() {\n        return \"text/plain\";\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        if (mBodyBytes == null)\n            mBodyBytes = string.getBytes();\n        return mBodyBytes.length;\n    }\n\n    @Override\n    public String toString() {\n        return string;\n    }\n\n    @Override\n    public String get() {\n        return toString();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/StringPart.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic class StringPart extends StreamPart {\n    String value;\n    public StringPart(String name, String value) {\n        super(name, value.getBytes().length, null);\n        this.value = value;\n    }\n\n    @Override\n    protected InputStream getInputStream() throws IOException {\n        return new ByteArrayInputStream(value.getBytes());\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    @Override\n    public String toString() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/body/UrlEncodedFormBody.java",
    "content": "package com.jeffmony.async.http.body;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.NameValuePair;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.util.List;\n\npublic class UrlEncodedFormBody implements AsyncHttpRequestBody<Multimap> {\n    private Multimap mParameters;\n    private byte[] mBodyBytes;\n\n    public UrlEncodedFormBody(Multimap parameters) {\n        mParameters = parameters;\n    }\n\n    public UrlEncodedFormBody(List<NameValuePair> parameters) {\n        mParameters = new Multimap(parameters);\n    }\n\n    private void buildData() {\n        boolean first = true;\n        StringBuilder b = new StringBuilder();\n        try {\n            for (NameValuePair pair: mParameters) {\n                if (pair.getValue() == null)\n                    continue;\n                if (!first)\n                    b.append('&');\n                first = false;\n\n                b.append(URLEncoder.encode(pair.getName(), \"UTF-8\"));\n                b.append('=');\n                b.append(URLEncoder.encode(pair.getValue(), \"UTF-8\"));\n            }\n            mBodyBytes = b.toString().getBytes(\"UTF-8\");\n        }\n        catch (UnsupportedEncodingException e) {\n            throw new AssertionError(e);\n        }\n    }\n    \n    @Override\n    public void write(AsyncHttpRequest request, final DataSink response, final CompletedCallback completed) {\n        if (mBodyBytes == null)\n            buildData();\n        Util.writeAll(response, mBodyBytes, completed);\n    }\n\n    public static final String CONTENT_TYPE = \"application/x-www-form-urlencoded\";\n    @Override\n    public String getContentType() {\n        return CONTENT_TYPE + \"; charset=utf-8\";\n    }\n\n    @Override\n    public void parse(DataEmitter emitter, final CompletedCallback completed) {\n        final ByteBufferList data = new ByteBufferList();\n        emitter.setDataCallback(new DataCallback() {\n            @Override\n            public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                bb.get(data);\n            }\n        });\n        emitter.setEndCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                try {\n                    if (ex != null)\n                        throw ex;\n                    mParameters = Multimap.parseUrlEncoded(data.readString());\n                }\n                catch (Exception e) {\n                    completed.onCompleted(e);\n                    return;\n                }\n                completed.onCompleted(null);\n            }\n        });\n    }\n\n    public UrlEncodedFormBody() {\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return true;\n    }\n\n    @Override\n    public int length() {\n        if (mBodyBytes == null)\n            buildData();\n        return mBodyBytes.length;\n    }\n\n    @Override\n    public Multimap get() {\n        return mParameters;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/HeaderParser.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nfinal class HeaderParser {\n\n    public interface CacheControlHandler {\n        void handle(String directive, String parameter);\n    }\n\n    /**\n     * Parse a comma-separated list of cache control header values.\n     */\n    public static void parseCacheControl(String value, CacheControlHandler handler) {\n        if (value == null)\n            return;\n        int pos = 0;\n        while (pos < value.length()) {\n            int tokenStart = pos;\n            pos = skipUntil(value, pos, \"=,\");\n            String directive = value.substring(tokenStart, pos).trim();\n\n            if (pos == value.length() || value.charAt(pos) == ',') {\n                pos++; // consume ',' (if necessary)\n                handler.handle(directive, null);\n                continue;\n            }\n\n            pos++; // consume '='\n            pos = skipWhitespace(value, pos);\n\n            String parameter;\n\n            // quoted string\n            if (pos < value.length() && value.charAt(pos) == '\\\"') {\n                pos++; // consume '\"' open quote\n                int parameterStart = pos;\n                pos = skipUntil(value, pos, \"\\\"\");\n                parameter = value.substring(parameterStart, pos);\n                pos++; // consume '\"' close quote (if necessary)\n\n            // unquoted string\n            } else {\n                int parameterStart = pos;\n                pos = skipUntil(value, pos, \",\");\n                parameter = value.substring(parameterStart, pos).trim();\n            }\n\n            handler.handle(directive, parameter);\n        }\n    }\n\n    /**\n     * Returns the next index in {@code input} at or after {@code pos} that\n     * contains a character from {@code characters}. Returns the input length if\n     * none of the requested characters can be found.\n     */\n    private static int skipUntil(String input, int pos, String characters) {\n        for (; pos < input.length(); pos++) {\n            if (characters.indexOf(input.charAt(pos)) != -1) {\n                break;\n            }\n        }\n        return pos;\n    }\n\n    /**\n     * Returns the next non-whitespace character in {@code input} that is white\n     * space. Result is undefined if input contains newline characters.\n     */\n    private static int skipWhitespace(String input, int pos) {\n        for (; pos < input.length(); pos++) {\n            char c = input.charAt(pos);\n            if (c != ' ' && c != '\\t') {\n                break;\n            }\n        }\n        return pos;\n    }\n\n    /**\n     * Returns {@code value} as a positive integer, or 0 if it is negative, or\n     * -1 if it cannot be parsed.\n     */\n    public static int parseSeconds(String value) {\n        try {\n            long seconds = Long.parseLong(value);\n            if (seconds > Integer.MAX_VALUE) {\n                return Integer.MAX_VALUE;\n            } else if (seconds < 0) {\n                return 0;\n            } else {\n                return (int) seconds;\n            }\n        } catch (NumberFormatException e) {\n            return -1;\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/Objects.java",
    "content": "/*\n * Copyright (C) 2010 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nfinal class Objects {\n    private Objects() {}\n\n    /**\n     * Returns true if two possibly-null objects are equal.\n     */\n    public static boolean equal(Object a, Object b) {\n        return a == b || (a != null && a.equals(b));\n    }\n\n    public static int hashCode(Object o) {\n        return (o == null) ? 0 : o.hashCode();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/RawHeaders.java",
    "content": "package com.jeffmony.async.http.cache;\n\n/*\n *  Licensed to the Apache Software Foundation (ASF) under one or more\n *  contributor license agreements.  See the NOTICE file distributed with\n *  this work for additional information regarding copyright ownership.\n *  The ASF licenses this file to You under the Apache License, Version 2.0\n *  (the \"License\"); you may not use this file except in compliance with\n *  the License.  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 android.text.TextUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.TreeMap;\n\n/**\n * The HTTP status and unparsed header fields of a single HTTP message. Values\n * are represented as uninterpreted strings; use {@link RequestHeaders} and\n * {@link ResponseHeaders} for interpreted headers. This class maintains the\n * order of the header fields within the HTTP message.\n *\n * <p>This class tracks fields line-by-line. A field with multiple comma-\n * separated values on the same line will be treated as a field with a single\n * value by this class. It is the caller's responsibility to detect and split\n * on commas if their field permits multiple values. This simplifies use of\n * single-valued fields whose values routinely contain commas, such as cookies\n * or dates.\n *\n * <p>This class trims whitespace from values. It never returns values with\n * leading or trailing whitespace.\n */\nfinal class RawHeaders {\n    private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {\n        @Override\n        public int compare(String a, String b) {\n            if (a == b) {\n                return 0;\n            } else if (a == null) {\n                return -1;\n            } else if (b == null) {\n                return 1;\n            } else {\n                return String.CASE_INSENSITIVE_ORDER.compare(a, b);\n            }\n        }\n    };\n\n    private final List<String> namesAndValues = new ArrayList<String>(20);\n    private String statusLine;\n    private int httpMinorVersion = 1;\n    private int responseCode = -1;\n    private String responseMessage;\n\n    public RawHeaders() {}\n\n    public RawHeaders(RawHeaders copyFrom) {\n        copy(copyFrom);\n    }\n\n    public void copy(RawHeaders copyFrom) {\n        namesAndValues.addAll(copyFrom.namesAndValues);\n        statusLine = copyFrom.statusLine;\n        httpMinorVersion = copyFrom.httpMinorVersion;\n        responseCode = copyFrom.responseCode;\n        responseMessage = copyFrom.responseMessage;\n    }\n\n    /**\n     * Sets the response status line (like \"HTTP/1.0 200 OK\") or request line\n     * (like \"GET / HTTP/1.1\").\n     */\n    public void setStatusLine(String statusLine) {\n        statusLine = statusLine.trim();\n        this.statusLine = statusLine;\n\n        if (statusLine == null || !statusLine.startsWith(\"HTTP/\")) {\n            return;\n        }\n        statusLine = statusLine.trim();\n        int mark = statusLine.indexOf(\" \") + 1;\n        if (mark == 0) {\n            return;\n        }\n        if (statusLine.charAt(mark - 2) != '1') {\n            this.httpMinorVersion = 0;\n        }\n        int last = mark + 3;\n        if (last > statusLine.length()) {\n            last = statusLine.length();\n        }\n        this.responseCode = Integer.parseInt(statusLine.substring(mark, last));\n        if (last + 1 <= statusLine.length()) {\n            this.responseMessage = statusLine.substring(last + 1);\n        }\n    }\n\n    public String getStatusLine() {\n        return statusLine;\n    }\n\n    /**\n     * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0\n     * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.\n     */\n    public int getHttpMinorVersion() {\n        return httpMinorVersion != -1 ? httpMinorVersion : 1;\n    }\n\n    /**\n     * Returns the HTTP status code or -1 if it is unknown.\n     */\n    public int getResponseCode() {\n        return responseCode;\n    }\n\n    /**\n     * Returns the HTTP status message or null if it is unknown.\n     */\n    public String getResponseMessage() {\n        return responseMessage;\n    }\n\n    /**\n     * Add an HTTP header line containing a field name, a literal colon, and a\n     * value.\n     */\n    public void addLine(String line) {\n        int index = line.indexOf(\":\");\n        if (index == -1) {\n            add(\"\", line);\n        } else {\n            add(line.substring(0, index), line.substring(index + 1));\n        }\n    }\n\n    /**\n     * Add a field with the specified value.\n     */\n    public void add(String fieldName, String value) {\n        if (fieldName == null) {\n            throw new IllegalArgumentException(\"fieldName == null\");\n        }\n        if (value == null) {\n            /*\n             * Given null values, the RI sends a malformed field line like\n             * \"Accept\\r\\n\". For platform compatibility and HTTP compliance, we\n             * print a warning and ignore null values.\n             */\n            System.err.println(\"Ignoring HTTP header field '\" + fieldName + \"' because its value is null\");\n            return;\n        }\n        namesAndValues.add(fieldName);\n        namesAndValues.add(value.trim());\n    }\n\n    public void removeAll(String fieldName) {\n        for (int i = 0; i < namesAndValues.size(); i += 2) {\n            if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {\n                namesAndValues.remove(i); // field name\n                namesAndValues.remove(i); // value\n            }\n        }\n    }\n\n    public void addAll(String fieldName, List<String> headerFields) {\n        for (String value : headerFields) {\n            add(fieldName, value);\n        }\n    }\n\n    /**\n     * Set a field with the specified value. If the field is not found, it is\n     * added. If the field is found, the existing values are replaced.\n     */\n    public void set(String fieldName, String value) {\n        removeAll(fieldName);\n        add(fieldName, value);\n    }\n\n    /**\n     * Returns the number of field values.\n     */\n    public int length() {\n        return namesAndValues.size() / 2;\n    }\n\n    /**\n     * Returns the field at {@code position} or null if that is out of range.\n     */\n    public String getFieldName(int index) {\n        int fieldNameIndex = index * 2;\n        if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {\n            return null;\n        }\n        return namesAndValues.get(fieldNameIndex);\n    }\n\n    /**\n     * Returns the value at {@code index} or null if that is out of range.\n     */\n    public String getValue(int index) {\n        int valueIndex = index * 2 + 1;\n        if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {\n            return null;\n        }\n        return namesAndValues.get(valueIndex);\n    }\n\n    /**\n     * Returns the last value corresponding to the specified field, or null.\n     */\n    public String get(String fieldName) {\n        for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {\n            if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {\n                return namesAndValues.get(i + 1);\n            }\n        }\n        return null;\n    }\n\n    /**\n     * @param fieldNames a case-insensitive set of HTTP header field names.\n     */\n    public RawHeaders getAll(Set<String> fieldNames) {\n        RawHeaders result = new RawHeaders();\n        for (int i = 0; i < namesAndValues.size(); i += 2) {\n            String fieldName = namesAndValues.get(i);\n            if (fieldNames.contains(fieldName)) {\n                result.add(fieldName, namesAndValues.get(i + 1));\n            }\n        }\n        return result;\n    }\n\n    public String toHeaderString() {\n        StringBuilder result = new StringBuilder(256);\n        result.append(statusLine).append(\"\\r\\n\");\n        for (int i = 0; i < namesAndValues.size(); i += 2) {\n            result.append(namesAndValues.get(i)).append(\": \")\n                    .append(namesAndValues.get(i + 1)).append(\"\\r\\n\");\n        }\n        result.append(\"\\r\\n\");\n        return result.toString();\n    }\n\n    /**\n     * Returns an immutable map containing each field to its list of values. The\n     * status line is mapped to null.\n     */\n    public Map<String, List<String>> toMultimap() {\n        Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);\n        for (int i = 0; i < namesAndValues.size(); i += 2) {\n            String fieldName = namesAndValues.get(i);\n            String value = namesAndValues.get(i + 1);\n\n            List<String> allValues = new ArrayList<String>();\n            List<String> otherValues = result.get(fieldName);\n            if (otherValues != null) {\n                allValues.addAll(otherValues);\n            }\n            allValues.add(value);\n            result.put(fieldName, Collections.unmodifiableList(allValues));\n        }\n        if (statusLine != null) {\n            result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));\n        }\n        return Collections.unmodifiableMap(result);\n    }\n\n    /**\n     * Creates a new instance from the given map of fields to values. If\n     * present, the null field's last element will be used to set the status\n     * line.\n     */\n    public static RawHeaders fromMultimap(Map<String, List<String>> map) {\n        RawHeaders result = new RawHeaders();\n        for (Entry<String, List<String>> entry : map.entrySet()) {\n            String fieldName = entry.getKey();\n            List<String> values = entry.getValue();\n            if (fieldName != null) {\n                result.addAll(fieldName, values);\n            } else if (!values.isEmpty()) {\n                result.setStatusLine(values.get(values.size() - 1));\n            }\n        }\n        return result;\n    }\n\n    public static RawHeaders parse(String payload) {\n        String[] lines = payload.split(\"\\n\");\n\n        RawHeaders headers = new RawHeaders();\n        for (String line: lines) {\n            line = line.trim();\n            if (TextUtils.isEmpty(line))\n                continue;\n\n            if (headers.getStatusLine() == null)\n                headers.setStatusLine(line);\n            else\n                headers.addLine(line);\n        }\n        return headers;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/RequestHeaders.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nimport android.net.Uri;\n\nimport com.jeffmony.async.http.HttpDate;\n\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Parsed HTTP request headers.\n */\nfinal class RequestHeaders {\n    private final Uri uri;\n    private final RawHeaders headers;\n\n    /** Don't use a cache to satisfy this request. */\n    private boolean noCache;\n    private int maxAgeSeconds = -1;\n    private int maxStaleSeconds = -1;\n    private int minFreshSeconds = -1;\n\n    /**\n     * This field's name \"only-if-cached\" is misleading. It actually means \"do\n     * not use the network\". It is set by a client who only wants to make a\n     * request if it can be fully satisfied by the cache. Cached responses that\n     * would require validation (ie. conditional gets) are not permitted if this\n     * header is set.\n     */\n    private boolean onlyIfCached;\n\n    /**\n     * True if the request contains an authorization field. Although this isn't\n     * necessarily a shared cache, it follows the spec's strict requirements for\n     * shared caches.\n     */\n    private boolean hasAuthorization;\n\n    private int contentLength = -1;\n    private String transferEncoding;\n    private String userAgent;\n    private String host;\n    private String connection;\n    private String acceptEncoding;\n    private String contentType;\n    private String ifModifiedSince;\n    private String ifNoneMatch;\n    private String proxyAuthorization;\n\n    public RequestHeaders(Uri uri, RawHeaders headers) {\n        this.uri = uri;\n        this.headers = headers;\n\n        HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {\n            @Override\n            public void handle(String directive, String parameter) {\n                if (directive.equalsIgnoreCase(\"no-cache\")) {\n                    noCache = true;\n                } else if (directive.equalsIgnoreCase(\"max-age\")) {\n                    maxAgeSeconds = HeaderParser.parseSeconds(parameter);\n                } else if (directive.equalsIgnoreCase(\"max-stale\")) {\n                    maxStaleSeconds = HeaderParser.parseSeconds(parameter);\n                } else if (directive.equalsIgnoreCase(\"min-fresh\")) {\n                    minFreshSeconds = HeaderParser.parseSeconds(parameter);\n                } else if (directive.equalsIgnoreCase(\"only-if-cached\")) {\n                    onlyIfCached = true;\n                }\n            }\n        };\n\n        for (int i = 0; i < headers.length(); i++) {\n            String fieldName = headers.getFieldName(i);\n            String value = headers.getValue(i);\n            if (\"Cache-Control\".equalsIgnoreCase(fieldName)) {\n                HeaderParser.parseCacheControl(value, handler);\n            } else if (\"Pragma\".equalsIgnoreCase(fieldName)) {\n                if (value.equalsIgnoreCase(\"no-cache\")) {\n                    noCache = true;\n                }\n            } else if (\"If-None-Match\".equalsIgnoreCase(fieldName)) {\n                ifNoneMatch = value;\n            } else if (\"If-Modified-Since\".equalsIgnoreCase(fieldName)) {\n                ifModifiedSince = value;\n            } else if (\"Authorization\".equalsIgnoreCase(fieldName)) {\n                hasAuthorization = true;\n            } else if (\"Content-Length\".equalsIgnoreCase(fieldName)) {\n                try {\n                    contentLength = Integer.parseInt(value);\n                } catch (NumberFormatException ignored) {\n                }\n            } else if (\"Transfer-Encoding\".equalsIgnoreCase(fieldName)) {\n                transferEncoding = value;\n            } else if (\"User-Agent\".equalsIgnoreCase(fieldName)) {\n                userAgent = value;\n            } else if (\"Host\".equalsIgnoreCase(fieldName)) {\n                host = value;\n            } else if (\"Connection\".equalsIgnoreCase(fieldName)) {\n                connection = value;\n            } else if (\"Accept-Encoding\".equalsIgnoreCase(fieldName)) {\n                acceptEncoding = value;\n            } else if (\"Content-Type\".equalsIgnoreCase(fieldName)) {\n                contentType = value;\n            } else if (\"Proxy-Authorization\".equalsIgnoreCase(fieldName)) {\n                proxyAuthorization = value;\n            }\n        }\n    }\n\n    public boolean isChunked() {\n        return \"chunked\".equalsIgnoreCase(transferEncoding);\n    }\n\n    public boolean hasConnectionClose() {\n        return \"close\".equalsIgnoreCase(connection);\n    }\n\n    public Uri getUri() {\n        return uri;\n    }\n\n    public RawHeaders getHeaders() {\n        return headers;\n    }\n\n    public boolean isNoCache() {\n        return noCache;\n    }\n\n    public int getMaxAgeSeconds() {\n        return maxAgeSeconds;\n    }\n\n    public int getMaxStaleSeconds() {\n        return maxStaleSeconds;\n    }\n\n    public int getMinFreshSeconds() {\n        return minFreshSeconds;\n    }\n\n    public boolean isOnlyIfCached() {\n        return onlyIfCached;\n    }\n\n    public boolean hasAuthorization() {\n        return hasAuthorization;\n    }\n\n    public int getContentLength() {\n        return contentLength;\n    }\n\n    public String getTransferEncoding() {\n        return transferEncoding;\n    }\n\n    public String getUserAgent() {\n        return userAgent;\n    }\n\n    public String getHost() {\n        return host;\n    }\n\n    public String getConnection() {\n        return connection;\n    }\n\n    public String getAcceptEncoding() {\n        return acceptEncoding;\n    }\n\n    public String getContentType() {\n        return contentType;\n    }\n\n    public String getIfModifiedSince() {\n        return ifModifiedSince;\n    }\n\n    public String getIfNoneMatch() {\n        return ifNoneMatch;\n    }\n\n    public String getProxyAuthorization() {\n        return proxyAuthorization;\n    }\n\n    public void setChunked() {\n        if (this.transferEncoding != null) {\n            headers.removeAll(\"Transfer-Encoding\");\n        }\n        headers.add(\"Transfer-Encoding\", \"chunked\");\n        this.transferEncoding = \"chunked\";\n    }\n\n    public void setContentLength(int contentLength) {\n        if (this.contentLength != -1) {\n            headers.removeAll(\"Content-Length\");\n        }\n        if (contentLength != -1) {\n            headers.add(\"Content-Length\", Integer.toString(contentLength));\n        }\n        this.contentLength = contentLength;\n    }\n\n    public void setUserAgent(String userAgent) {\n        if (this.userAgent != null) {\n            headers.removeAll(\"User-Agent\");\n        }\n        headers.add(\"User-Agent\", userAgent);\n        this.userAgent = userAgent;\n    }\n\n    public void setHost(String host) {\n        if (this.host != null) {\n            headers.removeAll(\"Host\");\n        }\n        headers.add(\"Host\", host);\n        this.host = host;\n    }\n\n    public void setConnection(String connection) {\n        if (this.connection != null) {\n            headers.removeAll(\"Connection\");\n        }\n        headers.add(\"Connection\", connection);\n        this.connection = connection;\n    }\n\n    public void setAcceptEncoding(String acceptEncoding) {\n        if (this.acceptEncoding != null) {\n            headers.removeAll(\"Accept-Encoding\");\n        }\n        headers.add(\"Accept-Encoding\", acceptEncoding);\n        this.acceptEncoding = acceptEncoding;\n    }\n\n    public void setContentType(String contentType) {\n        if (this.contentType != null) {\n            headers.removeAll(\"Content-Type\");\n        }\n        headers.add(\"Content-Type\", contentType);\n        this.contentType = contentType;\n    }\n\n    public void setIfModifiedSince(Date date) {\n        if (ifModifiedSince != null) {\n            headers.removeAll(\"If-Modified-Since\");\n        }\n        String formattedDate = HttpDate.format(date);\n        headers.add(\"If-Modified-Since\", formattedDate);\n        ifModifiedSince = formattedDate;\n    }\n\n    public void setIfNoneMatch(String ifNoneMatch) {\n        if (this.ifNoneMatch != null) {\n            headers.removeAll(\"If-None-Match\");\n        }\n        headers.add(\"If-None-Match\", ifNoneMatch);\n        this.ifNoneMatch = ifNoneMatch;\n    }\n\n    /**\n     * Returns true if the request contains conditions that save the server from\n     * sending a response that the client has locally. When the caller adds\n     * conditions, this cache won't participate in the request.\n     */\n    public boolean hasConditions() {\n        return ifModifiedSince != null || ifNoneMatch != null;\n    }\n\n    public void addCookies(Map<String, List<String>> allCookieHeaders) {\n        for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {\n            String key = entry.getKey();\n            if (\"Cookie\".equalsIgnoreCase(key) || \"Cookie2\".equalsIgnoreCase(key)) {\n                headers.addAll(key, entry.getValue());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseCacheMiddleware.java",
    "content": "package com.jeffmony.async.http.cache;\n\nimport android.net.Uri;\nimport android.util.Base64;\n\nimport com.jeffmony.async.AsyncSSLSocket;\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.future.Cancellable;\nimport com.jeffmony.async.future.SimpleCancellable;\nimport com.jeffmony.async.http.AsyncHttpClient;\nimport com.jeffmony.async.http.AsyncHttpClientMiddleware;\nimport com.jeffmony.async.http.AsyncHttpGet;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.SimpleMiddleware;\nimport com.jeffmony.async.util.Allocator;\nimport com.jeffmony.async.util.Charsets;\nimport com.jeffmony.async.util.FileCache;\nimport com.jeffmony.async.util.StreamUtility;\n\nimport java.io.BufferedWriter;\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.net.CacheResponse;\nimport java.nio.ByteBuffer;\nimport java.security.cert.Certificate;\nimport java.security.cert.CertificateEncodingException;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport javax.net.ssl.SSLEngine;\n\npublic class ResponseCacheMiddleware extends SimpleMiddleware {\n    public static final int ENTRY_METADATA = 0;\n    public static final int ENTRY_BODY = 1;\n    public static final int ENTRY_COUNT = 2;\n    public static final String SERVED_FROM = \"X-Served-From\";\n    public static final String CONDITIONAL_CACHE = \"conditional-cache\";\n    public static final String CACHE = \"cache\";\n    private static final String LOGTAG = \"AsyncHttpCache\";\n    private boolean caching = true;\n    private int writeSuccessCount;\n    private int writeAbortCount;\n    private FileCache cache;\n    private AsyncServer server;\n    private int conditionalCacheHitCount;\n    private int cacheHitCount;\n    private int networkCount;\n    private int cacheStoreCount;\n\n    private ResponseCacheMiddleware() {\n    }\n\n    public static ResponseCacheMiddleware addCache(AsyncHttpClient client, File cacheDir, long size) throws IOException {\n        for (AsyncHttpClientMiddleware middleware: client.getMiddleware()) {\n            if (middleware instanceof ResponseCacheMiddleware)\n                throw new IOException(\"Response cache already added to http client\");\n        }\n        ResponseCacheMiddleware ret = new ResponseCacheMiddleware();\n        ret.server = client.getServer();\n        ret.cache = new FileCache(cacheDir, size, false);\n        client.insertMiddleware(ret);\n        return ret;\n    }\n\n    public FileCache getFileCache() {\n        return cache;\n    }\n    \n    public boolean getCaching() {\n        return caching;\n    }\n    \n    public void setCaching(boolean caching) {\n        this.caching = caching;\n    }\n\n    public void removeFromCache(Uri uri) {\n        String key = FileCache.toKeyString(uri);\n        getFileCache().remove(key);\n    }\n\n    // step 1) see if we can serve request from the cache directly.\n    // also see if this can be turned into a conditional cache request.\n    @Override\n    public Cancellable getSocket(final GetSocketData data) {\n        RequestHeaders requestHeaders = new RequestHeaders(data.request.getUri(), RawHeaders.fromMultimap(data.request.getHeaders().getMultiMap()));\n        data.state.put(\"request-headers\", requestHeaders);\n\n        if (cache == null || !caching || requestHeaders.isNoCache()) {\n            networkCount++;\n            return null;\n        }\n\n        String key = FileCache.toKeyString(data.request.getUri());\n        FileInputStream[] snapshot = null;\n        long contentLength;\n        Entry entry;\n        try {\n            snapshot = cache.get(key, ENTRY_COUNT);\n            if (snapshot == null) {\n                networkCount++;\n                return null;\n            }\n            contentLength = snapshot[ENTRY_BODY].available();\n            entry = new Entry(snapshot[ENTRY_METADATA]);\n        }\n        catch (IOException e) {\n            // Give up because the cache cannot be read.\n            networkCount++;\n            StreamUtility.closeQuietly(snapshot);\n            return null;\n        }\n\n        // verify the entry matches\n        if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getMultiMap())) {\n            networkCount++;\n            StreamUtility.closeQuietly(snapshot);\n            return null;\n        }\n\n        EntryCacheResponse candidate = new EntryCacheResponse(entry, snapshot[ENTRY_BODY]);\n\n        Map<String, List<String>> responseHeadersMap;\n        FileInputStream cachedResponseBody;\n        try {\n            responseHeadersMap = candidate.getHeaders();\n            cachedResponseBody = candidate.getBody();\n        }\n        catch (Exception e) {\n            networkCount++;\n            StreamUtility.closeQuietly(snapshot);\n            return null;\n        }\n        if (responseHeadersMap == null || cachedResponseBody == null) {\n            networkCount++;\n            StreamUtility.closeQuietly(snapshot);\n            return null;\n        }\n\n        RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap);\n        ResponseHeaders cachedResponseHeaders = new ResponseHeaders(data.request.getUri(), rawResponseHeaders);\n        rawResponseHeaders.set(\"Content-Length\", String.valueOf(contentLength));\n        rawResponseHeaders.removeAll(\"Content-Encoding\");\n        rawResponseHeaders.removeAll(\"Transfer-Encoding\");\n        cachedResponseHeaders.setLocalTimestamps(System.currentTimeMillis(), System.currentTimeMillis());\n\n        long now = System.currentTimeMillis();\n        ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);\n\n        if (responseSource == ResponseSource.CACHE) {\n            data.request.logi(\"Response retrieved from cache\");\n            final CachedSocket socket = entry.isHttps() ? new CachedSSLSocket(candidate, contentLength) : new CachedSocket(candidate, contentLength);\n            socket.pending.add(ByteBuffer.wrap(rawResponseHeaders.toHeaderString().getBytes()));\n\n            server.post(new Runnable() {\n                @Override\n                public void run() {\n                    data.connectCallback.onConnectCompleted(null, socket);\n                    socket.sendCachedDataOnNetworkThread();\n                }\n            });\n            cacheHitCount++;\n            data.state.put(\"socket-owner\", this);\n            SimpleCancellable ret = new SimpleCancellable();\n            ret.setComplete();\n            return ret;\n        }\n        else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {\n            data.request.logi(\"Response may be served from conditional cache\");\n            CacheData cacheData = new CacheData();\n            cacheData.snapshot = snapshot;\n            cacheData.contentLength = contentLength;\n            cacheData.cachedResponseHeaders = cachedResponseHeaders;\n            cacheData.candidate = candidate;\n            data.state.put(\"cache-data\", cacheData);\n            return null;\n        }\n        else {\n            data.request.logd(\"Response can not be served from cache\");\n            // NETWORK or other\n            networkCount++;\n            StreamUtility.closeQuietly(snapshot);\n            return null;\n        }\n    }\n\n    public int getConditionalCacheHitCount() {\n        return conditionalCacheHitCount;\n    }\n\n    public int getCacheHitCount() {\n        return cacheHitCount;\n    }\n    \n    public int getNetworkCount() {\n        return networkCount;\n    }\n\n    public int getCacheStoreCount() {\n        return cacheStoreCount;\n    }\n\n    // step 2) if this is a conditional cache request, serve it from the cache if necessary\n    // otherwise, see if it is cacheable\n    @Override\n    public void onBodyDecoder(OnBodyDecoderData data) {\n        CachedSocket cached = Util.getWrappedSocket(data.socket, CachedSocket.class);\n        if (cached != null) {\n            data.response.headers().set(SERVED_FROM, CACHE);\n            return;\n        }\n\n        CacheData cacheData = data.state.get(\"cache-data\");\n        RawHeaders rh = RawHeaders.fromMultimap(data.response.headers().getMultiMap());\n        rh.removeAll(\"Content-Length\");\n        rh.setStatusLine(String.format(Locale.ENGLISH, \"%s %s %s\", data.response.protocol(), data.response.code(), data.response.message()));\n        ResponseHeaders networkResponse = new ResponseHeaders(data.request.getUri(), rh);\n        data.state.put(\"response-headers\", networkResponse);\n        if (cacheData != null) {\n            if (cacheData.cachedResponseHeaders.validate(networkResponse)) {\n                data.request.logi(\"Serving response from conditional cache\");\n                ResponseHeaders combined = cacheData.cachedResponseHeaders.combine(networkResponse);\n                data.response.headers(new Headers(combined.getHeaders().toMultimap()));\n                data.response.code(combined.getHeaders().getResponseCode());\n                data.response.message(combined.getHeaders().getResponseMessage());\n\n                data.response.headers().set(SERVED_FROM, CONDITIONAL_CACHE);\n                conditionalCacheHitCount++;\n\n                CachedBodyEmitter bodySpewer = new CachedBodyEmitter(cacheData.candidate, cacheData.contentLength);\n                bodySpewer.setDataEmitter(data.bodyEmitter);\n                data.bodyEmitter = bodySpewer;\n                bodySpewer.sendCachedData();\n                return;\n            }\n\n            // did not validate, so fall through and cache the response\n            data.state.remove(\"cache-data\");\n            StreamUtility.closeQuietly(cacheData.snapshot);\n        }\n\n        if (!caching)\n            return;\n\n        RequestHeaders requestHeaders = data.state.get(\"request-headers\");\n        if (requestHeaders == null || !networkResponse.isCacheable(requestHeaders) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) {\n            /*\n             * Don't cache non-GET responses. We're technically allowed to cache\n             * HEAD requests and some POST requests, but the complexity of doing\n             * so is high and the benefit is low.\n             */\n            networkCount++;\n            data.request.logd(\"Response is not cacheable\");\n            return;\n        }\n\n        String key = FileCache.toKeyString(data.request.getUri());\n        RawHeaders varyHeaders = requestHeaders.getHeaders().getAll(networkResponse.getVaryFields());\n        Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, networkResponse.getHeaders());\n        BodyCacher cacher = new BodyCacher();\n        EntryEditor editor = new EntryEditor(key);\n        try {\n            entry.writeTo(editor);\n            // create the file\n            editor.newOutputStream(ENTRY_BODY);\n        }\n        catch (Exception e) {\n            // Log.e(LOGTAG, \"error\", e);\n            editor.abort();\n            networkCount++;\n            return;\n        }\n        cacher.editor = editor;\n\n        cacher.setDataEmitter(data.bodyEmitter);\n        data.bodyEmitter = cacher;\n\n        data.state.put(\"body-cacher\", cacher);\n        data.request.logd(\"Caching response\");\n        cacheStoreCount++;\n    }\n\n    // step 3: close up shop\n    @Override\n    public void onResponseComplete(OnResponseCompleteData data) {\n        CacheData cacheData = data.state.get(\"cache-data\");\n        if (cacheData != null && cacheData.snapshot != null)\n            StreamUtility.closeQuietly(cacheData.snapshot);\n\n        CachedSocket cachedSocket = Util.getWrappedSocket(data.socket, CachedSocket.class);\n        if (cachedSocket != null)\n            StreamUtility.closeQuietly((cachedSocket.cacheResponse).getBody());\n\n        BodyCacher cacher = data.state.get(\"body-cacher\");\n        if (cacher != null) {\n            if (data.exception != null)\n                cacher.abort();\n            else\n                cacher.commit();\n        }\n    }\n    \n    public void clear() {\n        if (cache != null) {\n            cache.clear();\n        }\n    }\n\n    public static class CacheData {\n        FileInputStream[] snapshot;\n        EntryCacheResponse candidate;\n        long contentLength;\n        ResponseHeaders cachedResponseHeaders;\n    }\n    \n    private static class BodyCacher extends FilteredDataEmitter {\n        EntryEditor editor;\n        ByteBufferList cached;\n\n        @Override\n        protected void report(Exception e) {\n            super.report(e);\n            if (e != null)\n                abort();\n        }\n\n        @Override\n        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n            if (cached != null) {\n                super.onDataAvailable(emitter, cached);\n                // couldn't emit it all, so just wait for another day...\n                if (cached.remaining() > 0)\n                    return;\n                cached = null;\n            }\n\n            // write to cache... any data not consumed needs to be retained for the next callback\n            ByteBufferList copy = new ByteBufferList();\n            try {\n                if (editor != null) {\n                    OutputStream outputStream = editor.newOutputStream(ENTRY_BODY);\n                    if (outputStream != null) {\n                        while (!bb.isEmpty()) {\n                            ByteBuffer b = bb.remove();\n                            try {\n                                ByteBufferList.writeOutputStream(outputStream, b);\n                            }\n                            finally {\n                                copy.add(b);\n                            }\n                        }\n                    }\n                    else {\n                        abort();\n                    }\n                }\n            }\n            catch (Exception e) {\n                abort();\n            }\n            finally {\n                bb.get(copy);\n                copy.get(bb);\n            }\n\n            super.onDataAvailable(emitter, bb);\n\n            if (editor != null && bb.remaining() > 0) {\n                cached = new ByteBufferList();\n                bb.get(cached);\n            }\n        }\n\n        @Override\n        public void close() {\n            abort();\n            super.close();\n        }\n\n        public void abort() {\n            if (editor != null) {\n                editor.abort();\n                editor = null;\n            }\n        }\n\n        public void commit() {\n            if (editor != null) {\n                editor.commit();\n                editor = null;\n            }\n        }\n    }\n\n    private static class CachedBodyEmitter extends FilteredDataEmitter {\n        EntryCacheResponse cacheResponse;\n        ByteBufferList pending = new ByteBufferList();\n        private boolean paused;\n        private Allocator allocator = new Allocator();\n        boolean allowEnd;\n        public CachedBodyEmitter(EntryCacheResponse cacheResponse, long contentLength) {\n            this.cacheResponse = cacheResponse;\n            allocator.setCurrentAlloc((int)contentLength);\n        }\n\n        Runnable sendCachedDataRunnable = new Runnable() {\n            @Override\n            public void run() {\n                sendCachedDataOnNetworkThread();\n            }\n        };\n\n        void sendCachedDataOnNetworkThread() {\n            if (pending.remaining() > 0) {\n                super.onDataAvailable(CachedBodyEmitter.this, pending);\n                if (pending.remaining() > 0)\n                    return;\n            }\n\n            // fill pending\n            try {\n                ByteBuffer buffer = allocator.allocate();\n                assert buffer.position() == 0;\n                FileInputStream din = cacheResponse.getBody();\n                int read = din.read(buffer.array(), buffer.arrayOffset(), buffer.capacity());\n                if (read == -1) {\n                    ByteBufferList.reclaim(buffer);\n                    allowEnd = true;\n                    report(null);\n                    return;\n                }\n                allocator.track(read);\n                buffer.limit(read);\n                pending.add(buffer);\n            }\n            catch (IOException e) {\n                allowEnd = true;\n                report(e);\n                return;\n            }\n            super.onDataAvailable(this, pending);\n            if (pending.remaining() > 0)\n                return;\n            // this limits max throughput to 256k (aka max alloc) * 100 per second...\n            // roughly 25MB/s\n            getServer().postDelayed(sendCachedDataRunnable, 10);\n        }\n\n        void sendCachedData() {\n            getServer().post(sendCachedDataRunnable);\n        }\n\n        @Override\n        public void resume() {\n            paused = false;\n            sendCachedData();\n        }\n\n        @Override\n        public boolean isPaused() {\n            return paused;\n        }\n\n        @Override\n        public void close() {\n            if (getServer().getAffinity() != Thread.currentThread()) {\n                getServer().post(new Runnable() {\n                    @Override\n                    public void run() {\n                        close();\n                    }\n                });\n                return;\n            }\n\n            pending.recycle();\n            StreamUtility.closeQuietly(cacheResponse.getBody());\n            super.close();\n        }\n\n        @Override\n        protected void report(Exception e) {\n            // a 304 response will immediate call report/end since there is no body.\n            // prevent this from happening by waiting for the actual body to be spit out.\n            if (!allowEnd)\n                return;\n            StreamUtility.closeQuietly(cacheResponse.getBody());\n            super.report(e);\n        }\n    }\n    \n    private static final class Entry {\n        private final String uri;\n        private final RawHeaders varyHeaders;\n        private final String requestMethod;\n        private final RawHeaders responseHeaders;\n        private final String cipherSuite;\n        private final Certificate[] peerCertificates;\n        private final Certificate[] localCertificates;\n\n        /*\n         * Reads an entry from an input stream. A typical entry looks like this:\n         *   http://google.com/foo\n         *   GET\n         *   2\n         *   Accept-Language: fr-CA\n         *   Accept-Charset: UTF-8\n         *   HTTP/1.1 200 OK\n         *   3\n         *   Content-Type: image/png\n         *   Content-Length: 100\n         *   Cache-Control: max-age=600\n         *\n         * A typical HTTPS file looks like this:\n         *   https://google.com/foo\n         *   GET\n         *   2\n         *   Accept-Language: fr-CA\n         *   Accept-Charset: UTF-8\n         *   HTTP/1.1 200 OK\n         *   3\n         *   Content-Type: image/png\n         *   Content-Length: 100\n         *   Cache-Control: max-age=600\n         *\n         *   AES_256_WITH_MD5\n         *   2\n         *   base64-encoded peerCertificate[0]\n         *   base64-encoded peerCertificate[1]\n         *   -1\n         *\n         * The file is newline separated. The first two lines are the URL and\n         * the request method. Next is the number of HTTP Vary request header\n         * lines, followed by those lines.\n         *\n         * Next is the response status line, followed by the number of HTTP\n         * response header lines, followed by those lines.\n         *\n         * HTTPS responses also contain SSL session information. This begins\n         * with a blank line, and then a line containing the cipher suite. Next\n         * is the length of the peer certificate chain. These certificates are\n         * base64-encoded and appear each on their own line. The next line\n         * contains the length of the local certificate chain. These\n         * certificates are also base64-encoded and appear each on their own\n         * line. A length of -1 is used to encode a null array.\n         */\n        public Entry(InputStream in) throws IOException {\n            StrictLineReader reader = null;\n            try {\n                reader = new StrictLineReader(in, Charsets.US_ASCII);\n                uri = reader.readLine();\n                requestMethod = reader.readLine();\n                varyHeaders = new RawHeaders();\n                int varyRequestHeaderLineCount = reader.readInt();\n                for (int i = 0; i < varyRequestHeaderLineCount; i++) {\n                    varyHeaders.addLine(reader.readLine());\n                }\n\n                responseHeaders = new RawHeaders();\n                responseHeaders.setStatusLine(reader.readLine());\n                int responseHeaderLineCount = reader.readInt();\n                for (int i = 0; i < responseHeaderLineCount; i++) {\n                    responseHeaders.addLine(reader.readLine());\n                }\n\n//                if (isHttps()) {\n//                    String blank = reader.readLine();\n//                    if (blank.length() != 0) {\n//                        throw new IOException(\"expected \\\"\\\" but was \\\"\" + blank + \"\\\"\");\n//                    }\n//                    cipherSuite = reader.readLine();\n//                    peerCertificates = readCertArray(reader);\n//                    localCertificates = readCertArray(reader);\n//                } else {\n                    cipherSuite = null;\n                    peerCertificates = null;\n                    localCertificates = null;\n//                }\n            } finally {\n                StreamUtility.closeQuietly(reader, in);\n            }\n        }\n\n        public Entry(Uri uri, RawHeaders varyHeaders, AsyncHttpRequest request, RawHeaders responseHeaders) {\n            this.uri = uri.toString();\n            this.varyHeaders = varyHeaders;\n            this.requestMethod = request.getMethod();\n            this.responseHeaders = responseHeaders;\n\n//            if (isHttps()) {\n//                HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;\n//                cipherSuite = httpsConnection.getCipherSuite();\n//                Certificate[] peerCertificatesNonFinal = null;\n//                try {\n//                    peerCertificatesNonFinal = httpsConnection.getServerCertificates();\n//                } catch (SSLPeerUnverifiedException ignored) {\n//                }\n//                peerCertificates = peerCertificatesNonFinal;\n//                localCertificates = httpsConnection.getLocalCertificates();\n//            } else {\n                cipherSuite = null;\n                peerCertificates = null;\n                localCertificates = null;\n//            }\n        }\n\n        public void writeTo(EntryEditor editor) throws IOException {\n            OutputStream out = editor.newOutputStream(ENTRY_METADATA);\n            Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.UTF_8));\n\n            writer.write(uri + '\\n');\n            writer.write(requestMethod + '\\n');\n            writer.write(Integer.toString(varyHeaders.length()) + '\\n');\n            for (int i = 0; i < varyHeaders.length(); i++) {\n                writer.write(varyHeaders.getFieldName(i) + \": \"\n                        + varyHeaders.getValue(i) + '\\n');\n            }\n\n            writer.write(responseHeaders.getStatusLine() + '\\n');\n            writer.write(Integer.toString(responseHeaders.length()) + '\\n');\n            for (int i = 0; i < responseHeaders.length(); i++) {\n                writer.write(responseHeaders.getFieldName(i) + \": \"\n                        + responseHeaders.getValue(i) + '\\n');\n            }\n\n            if (isHttps()) {\n                writer.write('\\n');\n                writer.write(cipherSuite + '\\n');\n                writeCertArray(writer, peerCertificates);\n                writeCertArray(writer, localCertificates);\n            }\n            writer.close();\n        }\n\n        private boolean isHttps() {\n            return uri.startsWith(\"https://\");\n        }\n\n        private Certificate[] readCertArray(StrictLineReader reader) throws IOException {\n            int length = reader.readInt();\n            if (length == -1) {\n                return null;\n            }\n            try {\n                CertificateFactory certificateFactory = CertificateFactory.getInstance(\"X.509\");\n                Certificate[] result = new Certificate[length];\n                for (int i = 0; i < result.length; i++) {\n                    String line = reader.readLine();\n                    byte[] bytes = Base64.decode(line, Base64.DEFAULT);\n                    result[i] = certificateFactory.generateCertificate(\n                            new ByteArrayInputStream(bytes));\n                }\n                return result;\n            } catch (CertificateException e) {\n                throw new IOException(e.getMessage());\n            }\n        }\n\n        private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {\n            if (certificates == null) {\n                writer.write(\"-1\\n\");\n                return;\n            }\n            try {\n                writer.write(Integer.toString(certificates.length) + '\\n');\n                for (Certificate certificate : certificates) {\n                    byte[] bytes = certificate.getEncoded();\n                    String line = Base64.encodeToString(bytes, Base64.DEFAULT);\n                    writer.write(line + '\\n');\n                }\n            } catch (CertificateEncodingException e) {\n                throw new IOException(e.getMessage());\n            }\n        }\n\n        public boolean matches(Uri uri, String requestMethod,\n                               Map<String, List<String>> requestHeaders) {\n            return this.uri.equals(uri.toString())\n                    && this.requestMethod.equals(requestMethod)\n                    && new ResponseHeaders(uri, responseHeaders)\n                            .varyMatches(varyHeaders.toMultimap(), requestHeaders);\n        }\n    }\n\n    static class EntryCacheResponse extends CacheResponse {\n        private final Entry entry;\n        private final FileInputStream snapshot;\n\n        public EntryCacheResponse(Entry entry, FileInputStream snapshot) {\n            this.entry = entry;\n            this.snapshot = snapshot;\n        }\n\n        @Override\n        public Map<String, List<String>> getHeaders() {\n            return entry.responseHeaders.toMultimap();\n        }\n\n        @Override\n        public FileInputStream getBody() {\n            return snapshot;\n        }\n    }\n\n    private class CachedSSLSocket extends CachedSocket implements AsyncSSLSocket {\n        public CachedSSLSocket(EntryCacheResponse cacheResponse, long contentLength) {\n            super(cacheResponse, contentLength);\n        }\n\n        @Override\n        public SSLEngine getSSLEngine() {\n            return null;\n        }\n\n        @Override\n        public X509Certificate[] getPeerCertificates() {\n            return null;\n        }\n    }\n\n    private class CachedSocket extends CachedBodyEmitter implements AsyncSocket {\n        boolean closed;\n        boolean open;\n        CompletedCallback closedCallback;\n        public CachedSocket(EntryCacheResponse cacheResponse, long contentLength) {\n            super(cacheResponse, contentLength);\n            allowEnd = true;\n        }\n\n        @Override\n        public void end() {\n        }\n\n        @Override\n        protected void report(Exception e) {\n            super.report(e);\n            if (closed)\n                return;\n            closed = true;\n            if (closedCallback != null)\n                closedCallback.onCompleted(e);\n        }\n\n        @Override\n        public void write(ByteBufferList bb) {\n            // it's gonna write headers and stuff... whatever\n            bb.recycle();\n        }\n\n        @Override\n        public WritableCallback getWriteableCallback() {\n            return null;\n        }\n\n        @Override\n        public void setWriteableCallback(WritableCallback handler) {\n        }\n\n        @Override\n        public boolean isOpen() {\n            return open;\n        }\n\n        @Override\n        public void close() {\n            open = false;\n        }\n\n        @Override\n        public CompletedCallback getClosedCallback() {\n            return closedCallback;\n        }\n\n        @Override\n        public void setClosedCallback(CompletedCallback handler) {\n            closedCallback = handler;\n        }\n\n        @Override\n        public AsyncServer getServer() {\n            return server;\n        }\n    }\n\n    class EntryEditor {\n        String key;\n        File[] temps;\n        FileOutputStream[] outs;\n        boolean done;\n        public EntryEditor(String key) {\n            this.key = key;\n            temps = cache.getTempFiles(ENTRY_COUNT);\n            outs = new FileOutputStream[ENTRY_COUNT];\n        }\n\n        void commit() {\n            StreamUtility.closeQuietly(outs);\n            if (done)\n                return;\n            cache.commitTempFiles(key, temps);\n            writeSuccessCount++;\n            done = true;\n        }\n\n        FileOutputStream newOutputStream(int index) throws IOException {\n            if (outs[index] == null)\n                outs[index] = new FileOutputStream(temps[index]);\n            return outs[index];\n        }\n\n        void abort() {\n            StreamUtility.closeQuietly(outs);\n            FileCache.removeFiles(temps);\n            if (done)\n                return;\n            writeAbortCount++;\n            done = true;\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseHeaders.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nimport android.net.Uri;\n\nimport com.jeffmony.async.http.HttpDate;\n\nimport java.net.HttpURLConnection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Parsed HTTP response headers.\n */\nfinal class ResponseHeaders {\n\n    /** HTTP header name for the local time when the request was sent. */\n    private static final String SENT_MILLIS = \"X-Android-Sent-Millis\";\n\n    /** HTTP header name for the local time when the response was received. */\n    private static final String RECEIVED_MILLIS = \"X-Android-Received-Millis\";\n\n    private final Uri uri;\n    private final RawHeaders headers;\n\n    /** The server's time when this response was served, if known. */\n    private Date servedDate;\n\n    /** The last modified date of the response, if known. */\n    private Date lastModified;\n\n    /**\n     * The expiration date of the response, if known. If both this field and the\n     * max age are set, the max age is preferred.\n     */\n    private Date expires;\n\n    /**\n     * Extension header set by HttpURLConnectionImpl specifying the timestamp\n     * when the HTTP request was first initiated.\n     */\n    private long sentRequestMillis;\n\n    /**\n     * Extension header set by HttpURLConnectionImpl specifying the timestamp\n     * when the HTTP response was first received.\n     */\n    private long receivedResponseMillis;\n\n    /**\n     * In the response, this field's name \"no-cache\" is misleading. It doesn't\n     * prevent us from caching the response; it only means we have to validate\n     * the response with the origin server before returning it. We can do this\n     * with a conditional get.\n     */\n    private boolean noCache;\n\n    /** If true, this response should not be cached. */\n    private boolean noStore;\n\n    /**\n     * The duration past the response's served date that it can be served\n     * without validation.\n     */\n    private int maxAgeSeconds = -1;\n\n    /**\n     * The \"s-maxage\" directive is the max age for shared caches. Not to be\n     * confused with \"max-age\" for non-shared caches, As in Firefox and Chrome,\n     * this directive is not honored by this cache.\n     */\n    private int sMaxAgeSeconds = -1;\n\n    /**\n     * This request header field's name \"only-if-cached\" is misleading. It\n     * actually means \"do not use the network\". It is set by a client who only\n     * wants to make a request if it can be fully satisfied by the cache.\n     * Cached responses that would require validation (ie. conditional gets) are\n     * not permitted if this header is set.\n     */\n    private boolean isPublic;\n    private boolean mustRevalidate;\n    private String etag;\n    private int ageSeconds = -1;\n\n    /** Case-insensitive set of field names. */\n    private Set<String> varyFields = Collections.emptySet();\n\n    private String contentEncoding;\n    private String transferEncoding;\n    private long contentLength = -1;\n    private String connection;\n    private String proxyAuthenticate;\n    private String wwwAuthenticate;\n\n    public ResponseHeaders(Uri uri, RawHeaders headers) {\n        this.uri = uri;\n        this.headers = headers;\n\n        HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {\n            @Override\n            public void handle(String directive, String parameter) {\n                if (directive.equalsIgnoreCase(\"no-cache\")) {\n                    noCache = true;\n                } else if (directive.equalsIgnoreCase(\"no-store\")) {\n                    noStore = true;\n                } else if (directive.equalsIgnoreCase(\"max-age\")) {\n                    maxAgeSeconds = HeaderParser.parseSeconds(parameter);\n                } else if (directive.equalsIgnoreCase(\"s-maxage\")) {\n                    sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);\n                } else if (directive.equalsIgnoreCase(\"public\")) {\n                    isPublic = true;\n                } else if (directive.equalsIgnoreCase(\"must-revalidate\")) {\n                    mustRevalidate = true;\n                }\n            }\n        };\n\n        for (int i = 0; i < headers.length(); i++) {\n            String fieldName = headers.getFieldName(i);\n            String value = headers.getValue(i);\n            if (\"Cache-Control\".equalsIgnoreCase(fieldName)) {\n                HeaderParser.parseCacheControl(value, handler);\n            } else if (\"Date\".equalsIgnoreCase(fieldName)) {\n                servedDate = HttpDate.parse(value);\n            } else if (\"Expires\".equalsIgnoreCase(fieldName)) {\n                expires = HttpDate.parse(value);\n            } else if (\"Last-Modified\".equalsIgnoreCase(fieldName)) {\n                lastModified = HttpDate.parse(value);\n            } else if (\"ETag\".equalsIgnoreCase(fieldName)) {\n                etag = value;\n            } else if (\"Pragma\".equalsIgnoreCase(fieldName)) {\n                if (value.equalsIgnoreCase(\"no-cache\")) {\n                    noCache = true;\n                }\n            } else if (\"Age\".equalsIgnoreCase(fieldName)) {\n                ageSeconds = HeaderParser.parseSeconds(value);\n            } else if (\"Vary\".equalsIgnoreCase(fieldName)) {\n                // Replace the immutable empty set with something we can mutate.\n                if (varyFields.isEmpty()) {\n                    varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);\n                }\n                for (String varyField : value.split(\",\")) {\n                    varyFields.add(varyField.trim().toLowerCase(Locale.US));\n                }\n            } else if (\"Content-Encoding\".equalsIgnoreCase(fieldName)) {\n                contentEncoding = value;\n            } else if (\"Transfer-Encoding\".equalsIgnoreCase(fieldName)) {\n                transferEncoding = value;\n            } else if (\"Content-Length\".equalsIgnoreCase(fieldName)) {\n                try {\n                    contentLength = Long.parseLong(value);\n                } catch (NumberFormatException ignored) {\n                }\n            } else if (\"Connection\".equalsIgnoreCase(fieldName)) {\n                connection = value;\n            } else if (\"Proxy-Authenticate\".equalsIgnoreCase(fieldName)) {\n                proxyAuthenticate = value;\n            } else if (\"WWW-Authenticate\".equalsIgnoreCase(fieldName)) {\n                wwwAuthenticate = value;\n            } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {\n                sentRequestMillis = Long.parseLong(value);\n            } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {\n                receivedResponseMillis = Long.parseLong(value);\n            }\n        }\n    }\n\n    public boolean isContentEncodingGzip() {\n        return \"gzip\".equalsIgnoreCase(contentEncoding);\n    }\n\n    public void stripContentEncoding() {\n        contentEncoding = null;\n        headers.removeAll(\"Content-Encoding\");\n    }\n\n    public boolean isChunked() {\n        return \"chunked\".equalsIgnoreCase(transferEncoding);\n    }\n\n    public boolean hasConnectionClose() {\n        return \"close\".equalsIgnoreCase(connection);\n    }\n\n    public Uri getUri() {\n        return uri;\n    }\n\n    public RawHeaders getHeaders() {\n        return headers;\n    }\n\n    public Date getServedDate() {\n        return servedDate;\n    }\n\n    public Date getLastModified() {\n        return lastModified;\n    }\n\n    public Date getExpires() {\n        return expires;\n    }\n\n    public boolean isNoCache() {\n        return noCache;\n    }\n\n    public boolean isNoStore() {\n        return noStore;\n    }\n\n    public int getMaxAgeSeconds() {\n        return maxAgeSeconds;\n    }\n\n    public int getSMaxAgeSeconds() {\n        return sMaxAgeSeconds;\n    }\n\n    public boolean isPublic() {\n        return isPublic;\n    }\n\n    public boolean isMustRevalidate() {\n        return mustRevalidate;\n    }\n\n    public String getEtag() {\n        return etag;\n    }\n\n    public Set<String> getVaryFields() {\n        return varyFields;\n    }\n\n    public String getContentEncoding() {\n        return contentEncoding;\n    }\n\n    public long getContentLength() {\n        return contentLength;\n    }\n\n    public String getConnection() {\n        return connection;\n    }\n\n    public String getProxyAuthenticate() {\n        return proxyAuthenticate;\n    }\n\n    public String getWwwAuthenticate() {\n        return wwwAuthenticate;\n    }\n\n    public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {\n        this.sentRequestMillis = sentRequestMillis;\n        headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));\n        this.receivedResponseMillis = receivedResponseMillis;\n        headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));\n    }\n\n    /**\n     * Returns the current age of the response, in milliseconds. The calculation\n     * is specified by RFC 2616, 13.2.3 Age Calculations.\n     */\n    private long computeAge(long nowMillis) {\n        long apparentReceivedAge = servedDate != null\n                ? Math.max(0, receivedResponseMillis - servedDate.getTime())\n                : 0;\n        long receivedAge = ageSeconds != -1\n                ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))\n                : apparentReceivedAge;\n        long responseDuration = receivedResponseMillis - sentRequestMillis;\n        long residentDuration = nowMillis - receivedResponseMillis;\n        return receivedAge + responseDuration + residentDuration;\n    }\n\n    /**\n     * Returns the number of milliseconds that the response was fresh for,\n     * starting from the served date.\n     */\n    private long computeFreshnessLifetime() {\n        if (maxAgeSeconds != -1) {\n            return TimeUnit.SECONDS.toMillis(maxAgeSeconds);\n        } else if (expires != null) {\n            long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;\n            long delta = expires.getTime() - servedMillis;\n            return delta > 0 ? delta : 0;\n        } else if (lastModified != null && uri.getEncodedQuery() == null) {\n            /*\n             * As recommended by the HTTP RFC and implemented in Firefox, the\n             * max age of a document should be defaulted to 10% of the\n             * document's age at the time it was served. Default expiration\n             * dates aren't used for URIs containing a query.\n             */\n            long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;\n            long delta = servedMillis - lastModified.getTime();\n            return delta > 0 ? (delta / 10) : 0;\n        }\n        return 0;\n    }\n\n    /**\n     * Returns true if computeFreshnessLifetime used a heuristic. If we used a\n     * heuristic to serve a cached response older than 24 hours, we are required\n     * to attach a warning.\n     */\n    private boolean isFreshnessLifetimeHeuristic() {\n        return maxAgeSeconds == -1 && expires == null;\n    }\n\n    /**\n     * Returns true if this response can be stored to later serve another\n     * request.\n     */\n    public boolean isCacheable(RequestHeaders request) {\n        /*\n         * Always go to network for uncacheable response codes (RFC 2616, 13.4),\n         * This implementation doesn't support caching partial content.\n         */\n        int responseCode = headers.getResponseCode();\n        if (responseCode != HttpURLConnection.HTTP_OK\n                && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE\n                && responseCode != HttpURLConnection.HTTP_MULT_CHOICE\n                && responseCode != HttpURLConnection.HTTP_MOVED_PERM\n                && responseCode != HttpURLConnection.HTTP_GONE) {\n            return false;\n        }\n\n        /*\n         * Responses to authorized requests aren't cacheable unless they include\n         * a 'public', 'must-revalidate' or 's-maxage' directive.\n         */\n        if (request.hasAuthorization()\n                && !isPublic\n                && !mustRevalidate\n                && sMaxAgeSeconds == -1) {\n            return false;\n        }\n\n        if (noStore) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Returns true if a Vary header contains an asterisk. Such responses cannot\n     * be cached.\n     */\n    public boolean hasVaryAll() {\n        return varyFields.contains(\"*\");\n    }\n\n    /**\n     * Returns true if none of the Vary headers on this response have changed\n     * between {@code cachedRequest} and {@code newRequest}.\n     */\n    public boolean varyMatches(Map<String, List<String>> cachedRequest,\n                               Map<String, List<String>> newRequest) {\n        for (String field : varyFields) {\n            if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Returns the source to satisfy {@code request} given this cached response.\n     */\n    public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {\n        /*\n         * If this response shouldn't have been stored, it should never be used\n         * as a response source. This check should be redundant as long as the\n         * persistence store is well-behaved and the rules are constant.\n         */\n        if (!isCacheable(request)) {\n            return ResponseSource.NETWORK;\n        }\n\n        if (request.isNoCache() || request.hasConditions()) {\n            return ResponseSource.NETWORK;\n        }\n\n        long ageMillis = computeAge(nowMillis);\n        long freshMillis = computeFreshnessLifetime();\n\n        if (request.getMaxAgeSeconds() != -1) {\n            freshMillis = Math.min(freshMillis,\n                    TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));\n        }\n\n        long minFreshMillis = 0;\n        if (request.getMinFreshSeconds() != -1) {\n            minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());\n        }\n\n        long maxStaleMillis = 0;\n        if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {\n            maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());\n        }\n\n        if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {\n            if (ageMillis + minFreshMillis >= freshMillis) {\n                headers.add(\"Warning\", \"110 HttpURLConnection \\\"Response is stale\\\"\");\n            }\n            /*\n             * not available in API 8\n            if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {\n            */\n            if (ageMillis > 24L * 60L * 60L * 1000L && isFreshnessLifetimeHeuristic()) {\n                headers.add(\"Warning\", \"113 HttpURLConnection \\\"Heuristic expiration\\\"\");\n            }\n            return ResponseSource.CACHE;\n        }\n\n        if (etag != null) {\n            request.setIfNoneMatch(etag);\n        }\n        else if (lastModified != null) {\n            request.setIfModifiedSince(lastModified);\n        } else if (servedDate != null) {\n            request.setIfModifiedSince(servedDate);\n        }\n\n\n        return request.hasConditions()\n                ? ResponseSource.CONDITIONAL_CACHE\n                : ResponseSource.NETWORK;\n    }\n\n    /**\n     * Returns true if this cached response should be used; false if the\n     * network response should be used.\n     */\n    public boolean validate(ResponseHeaders networkResponse) {\n        if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {\n            return true;\n        }\n\n        /*\n         * The HTTP spec says that if the network's response is older than our\n         * cached response, we may return the cache's response. Like Chrome (but\n         * unlike Firefox), this client prefers to return the newer response.\n         */\n        if (lastModified != null\n                && networkResponse.lastModified != null\n                && networkResponse.lastModified.getTime() < lastModified.getTime()) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Combines this cached header with a network header as defined by RFC 2616,\n     * 13.5.3.\n     */\n    public ResponseHeaders combine(ResponseHeaders network) {\n        RawHeaders result = new RawHeaders();\n\n        for (int i = 0; i < headers.length(); i++) {\n            String fieldName = headers.getFieldName(i);\n            String value = headers.getValue(i);\n            if (fieldName.equals(\"Warning\") && value.startsWith(\"1\")) {\n                continue; // drop 100-level freshness warnings\n            }\n            if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {\n                result.add(fieldName, value);\n            }\n        }\n\n        for (int i = 0; i < network.headers.length(); i++) {\n            String fieldName = network.headers.getFieldName(i);\n            if (isEndToEnd(fieldName)) {\n                result.add(fieldName, network.headers.getValue(i));\n            }\n        }\n\n        return new ResponseHeaders(uri, result);\n    }\n\n    /**\n     * Returns true if {@code fieldName} is an end-to-end HTTP header, as\n     * defined by RFC 2616, 13.5.1.\n     */\n    private static boolean isEndToEnd(String fieldName) {\n        return !fieldName.equalsIgnoreCase(\"Connection\")\n                && !fieldName.equalsIgnoreCase(\"Keep-Alive\")\n                && !fieldName.equalsIgnoreCase(\"Proxy-Authenticate\")\n                && !fieldName.equalsIgnoreCase(\"Proxy-Authorization\")\n                && !fieldName.equalsIgnoreCase(\"TE\")\n                && !fieldName.equalsIgnoreCase(\"Trailers\")\n                && !fieldName.equalsIgnoreCase(\"Transfer-Encoding\")\n                && !fieldName.equalsIgnoreCase(\"Upgrade\");\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/ResponseSource.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nenum ResponseSource {\n\n    /**\n     * Return the response from the cache immediately.\n     */\n    CACHE,\n\n    /**\n     * Make a conditional request to the host, returning the cache response if\n     * the cache is valid and the network response otherwise.\n     */\n    CONDITIONAL_CACHE,\n\n    /**\n     * Return the response from the network.\n     */\n    NETWORK;\n\n    public boolean requiresConnection() {\n        return this == CONDITIONAL_CACHE || this == NETWORK;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/cache/StrictLineReader.java",
    "content": "/*\n * Copyright (C) 2012 The Android Open Source Project\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\npackage com.jeffmony.async.http.cache;\n\nimport com.jeffmony.async.util.Charsets;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\n\n/**\n * Buffers input from an {@link InputStream} for reading lines.\n *\n * This class is used for buffered reading of lines. For purposes of this class, a line ends with\n * \"\\n\" or \"\\r\\n\". End of input is reported by throwing {@code EOFException}. Unterminated line at\n * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}\n * to detect it after catching the {@code EOFException}.\n *\n * This class is intended for reading input that strictly consists of lines, such as line-based\n * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction with\n * {@link java.io.InputStreamReader} provides similar functionality, this class uses different\n * end-of-input reporting and a more restrictive definition of a line.\n *\n * This class supports only charsets that encode '\\r' and '\\n' as a single byte with value 13\n * and 10, respectively, and the representation of no other character contains these values.\n * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.\n * The default charset is US_ASCII.\n */\nclass StrictLineReader implements Closeable {\n    private static final byte CR = (byte)'\\r';\n    private static final byte LF = (byte)'\\n';\n\n    private final InputStream in;\n\n    /*\n     * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end\n     * and the data in the range [pos, end) is buffered for reading. At end of input, if there is\n     * an unterminated line, we set end == -1, otherwise end == pos. If the underlying\n     * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.\n     */\n    private byte[] buf;\n    private int pos;\n    private int end;\n\n    /**\n     * Constructs a new {@code StrictLineReader} with the default capacity and charset.\n     *\n     * @param in the {@code InputStream} to read data from.\n     * @throws NullPointerException if {@code in} is null.\n     */\n    public StrictLineReader(InputStream in) {\n        this(in, 8192);\n    }\n\n    /**\n     * Constructs a new {@code LineReader} with the specified capacity and the default charset.\n     *\n     * @param in the {@code InputStream} to read data from.\n     * @param capacity the capacity of the buffer.\n     * @throws NullPointerException if {@code in} is null.\n     * @throws IllegalArgumentException for negative or zero {@code capacity}.\n     */\n    public StrictLineReader(InputStream in, int capacity) {\n        this(in, capacity, Charsets.US_ASCII);\n    }\n\n    /**\n     * Constructs a new {@code LineReader} with the specified charset and the default capacity.\n     *\n     * @param in the {@code InputStream} to read data from.\n     * @param charset the charset used to decode data.\n     *         Only US-ASCII, UTF-8 and ISO-8859-1 is supported.\n     * @throws NullPointerException if {@code in} or {@code charset} is null.\n     * @throws IllegalArgumentException if the specified charset is not supported.\n     */\n    public StrictLineReader(InputStream in, Charset charset) {\n        this(in, 8192, charset);\n    }\n\n    /**\n     * Constructs a new {@code LineReader} with the specified capacity and charset.\n     *\n     * @param in the {@code InputStream} to read data from.\n     * @param capacity the capacity of the buffer.\n     * @param charset the charset used to decode data.\n     *         Only US-ASCII, UTF-8 and ISO-8859-1 is supported.\n     * @throws NullPointerException if {@code in} or {@code charset} is null.\n     * @throws IllegalArgumentException if {@code capacity} is negative or zero\n     *         or the specified charset is not supported.\n     */\n    public StrictLineReader(InputStream in, int capacity, Charset charset) {\n        if (in == null) {\n            throw new NullPointerException(\"in == null\");\n        } else if (charset == null) {\n            throw new NullPointerException(\"charset == null\");\n        }\n        if (capacity < 0) {\n            throw new IllegalArgumentException(\"capacity <= 0\");\n        }\n        if (!(charset.equals(Charsets.US_ASCII) || charset.equals(Charsets.UTF_8))) {\n            throw new IllegalArgumentException(\"Unsupported encoding\");\n        }\n\n        this.in = in;\n        buf = new byte[capacity];\n    }\n\n    /**\n     * Closes the reader by closing the underlying {@code InputStream} and\n     * marking this reader as closed.\n     *\n     * @throws IOException for errors when closing the underlying {@code InputStream}.\n     */\n    @Override\n    public void close() throws IOException {\n        synchronized (in) {\n            if (buf != null) {\n                buf = null;\n                in.close();\n            }\n        }\n    }\n\n    /**\n     * Reads the next line. A line ends with {@code \"\\n\"} or {@code \"\\r\\n\"},\n     * this end of line marker is not included in the result.\n     *\n     * @return the next line from the input.\n     * @throws IOException for underlying {@code InputStream} errors.\n     * @throws EOFException for the end of source stream.\n     */\n    public String readLine() throws IOException {\n        synchronized (in) {\n            if (buf == null) {\n                throw new IOException(\"LineReader is closed\");\n            }\n\n            // Read more data if we are at the end of the buffered data.\n            // Though it's an error to read after an exception, we will let {@code fillBuf()}\n            // throw again if that happens; thus we need to handle end == -1 as well as end == pos.\n            if (pos >= end) {\n                fillBuf();\n            }\n            // Try to find LF in the buffered data and return the line if successful.\n            for (int i = pos; i != end; ++i) {\n                if (buf[i] == LF) {\n                    int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;\n                    String res = new String(buf, pos, lineEnd - pos);\n                    pos = i + 1;\n                    return res;\n                }\n            }\n\n            // Let's anticipate up to 80 characters on top of those already read.\n            ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {\n                @Override\n                public String toString() {\n                    int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;\n                    return new String(buf, 0, length);\n                }\n            };\n\n            while (true) {\n                out.write(buf, pos, end - pos);\n                // Mark unterminated line in case fillBuf throws EOFException or IOException.\n                end = -1;\n                fillBuf();\n                // Try to find LF in the buffered data and return the line if successful.\n                for (int i = pos; i != end; ++i) {\n                    if (buf[i] == LF) {\n                        if (i != pos) {\n                            out.write(buf, pos, i - pos);\n                        }\n                        pos = i + 1;\n                        return out.toString();\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Read an {@code int} from a line containing its decimal representation.\n     *\n     * @return the value of the {@code int} from the next line.\n     * @throws IOException for underlying {@code InputStream} errors or conversion error.\n     * @throws EOFException for the end of source stream.\n     */\n    public int readInt() throws IOException {\n        String intString = readLine();\n        try {\n            return Integer.parseInt(intString);\n        } catch (NumberFormatException e) {\n            throw new IOException(\"expected an int but was \\\"\" + intString + \"\\\"\");\n        }\n    }\n\n    /**\n     * Check whether there was an unterminated line at end of input after the line reader reported\n     * end-of-input with EOFException. The value is meaningless in any other situation.\n     *\n     * @return true if there was an unterminated line at end of input.\n     */\n    public boolean hasUnterminatedLine() {\n        return end == -1;\n    }\n\n    /**\n     * Reads new input data into the buffer. Call only with pos == end or end == -1,\n     * depending on the desired outcome if the function throws.\n     *\n     * @throws IOException for underlying {@code InputStream} errors.\n     * @throws EOFException for the end of source stream.\n     */\n    private void fillBuf() throws IOException {\n        int result = in.read(buf, 0, buf.length);\n        if (result == -1) {\n            throw new EOFException();\n        }\n        pos = 0;\n        end = result;\n    }\n}\n\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/callback/HttpConnectCallback.java",
    "content": "package com.jeffmony.async.http.callback;\n\nimport com.jeffmony.async.http.AsyncHttpResponse;\n\npublic interface HttpConnectCallback {\n    void onConnectCompleted(Exception ex, AsyncHttpResponse response);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/callback/RequestCallback.java",
    "content": "package com.jeffmony.async.http.callback;\n\nimport com.jeffmony.async.callback.ResultCallback;\nimport com.jeffmony.async.http.AsyncHttpResponse;\n\npublic interface RequestCallback<T> extends ResultCallback<AsyncHttpResponse, T> {\n    void onConnect(AsyncHttpResponse response);\n    void onProgress(AsyncHttpResponse response, long downloaded, long total);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedDataException.java",
    "content": "package com.jeffmony.async.http.filter;\n\npublic class ChunkedDataException extends Exception {\n    public ChunkedDataException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedInputFilter.java",
    "content": "package com.jeffmony.async.http.filter;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\nimport com.jeffmony.async.Util;\n\npublic class ChunkedInputFilter extends FilteredDataEmitter {\n    private int mChunkLength = 0;\n    private int mChunkLengthRemaining = 0;\n    private State mState = State.CHUNK_LEN;\n    \n    private enum State {\n        CHUNK_LEN,\n        CHUNK_LEN_CR,\n        CHUNK_LEN_CRLF,\n        CHUNK,\n        CHUNK_CR,\n        CHUNK_CRLF,\n        COMPLETE,\n        ERROR,\n    }\n    \n    private boolean checkByte(char b, char value) {\n        if (b != value) {\n            mState = State.ERROR;\n            report(new ChunkedDataException(value + \" was expected, got \" + (char)b));\n            return false;\n        }\n        return true;\n    }\n\n    private boolean checkLF(char b) {\n        return checkByte(b, '\\n');\n    }\n\n    private boolean checkCR(char b) {\n        return checkByte(b, '\\r');\n    }\n\n    @Override\n    protected void report(Exception e) {\n        if (e == null && mState != State.COMPLETE)\n            e = new ChunkedDataException(\"chunked input ended before final chunk\");\n        super.report(e);\n    }\n\n    ByteBufferList pending = new ByteBufferList();\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        if (mState == State.ERROR) {\n            bb.recycle();\n            return;\n        }\n\n        try {\n            while (bb.remaining() > 0) {\n                switch (mState) {\n                case CHUNK_LEN:\n                    char c = bb.getByteChar();\n                    if (c == '\\r') {\n                        mState = State.CHUNK_LEN_CR;\n                    }\n                    else {\n                        mChunkLength *= 16;\n                        if (c >= 'a' && c <= 'f')\n                            mChunkLength += (c - 'a' + 10);\n                        else if (c >= '0' && c <= '9')\n                            mChunkLength += c - '0';\n                        else if (c >= 'A' && c <= 'F')\n                            mChunkLength += (c - 'A' + 10);\n                        else {\n                            report(new ChunkedDataException(\"invalid chunk length: \" + c));\n                            return;\n                        }\n                    }\n                    mChunkLengthRemaining = mChunkLength;\n                    break;\n                case CHUNK_LEN_CR:\n                    if (!checkLF(bb.getByteChar()))\n                        return;\n                    mState = State.CHUNK;\n                    break;\n                case CHUNK:\n                    int remaining = bb.remaining();\n                    int reading = Math.min(mChunkLengthRemaining, remaining);\n                    mChunkLengthRemaining -= reading;\n                    if (mChunkLengthRemaining == 0) {\n                        mState = State.CHUNK_CR;\n                    }\n                    if (reading == 0)\n                        break;\n                    bb.get(pending, reading);\n                    Util.emitAllData(this, pending);\n                    break;\n                case CHUNK_CR:\n                    if (!checkCR(bb.getByteChar()))\n                        return;\n                    mState = State.CHUNK_CRLF;\n                    break;\n                case CHUNK_CRLF:\n                    if (!checkLF(bb.getByteChar()))\n                        return;\n                    if (mChunkLength > 0) {\n                        mState = State.CHUNK_LEN;\n                        \n                    }\n                    else {\n                        mState = State.COMPLETE;\n                        report(null);\n                    }\n                    mChunkLength = 0;\n                    break;\n                case COMPLETE:\n                    assert false;\n//                    Exception fail = new Exception(\"Continued receiving data after chunk complete\");\n//                    report(fail);\n                    return;\n                }\n            }\n        }\n        catch (Exception ex) {\n            report(ex);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/ChunkedOutputFilter.java",
    "content": "package com.jeffmony.async.http.filter;\n\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.FilteredDataSink;\n\nimport java.nio.ByteBuffer;\n\npublic class ChunkedOutputFilter extends FilteredDataSink {\n    public ChunkedOutputFilter(DataSink sink) {\n        super(sink);\n    }\n\n    @Override\n    public ByteBufferList filter(ByteBufferList bb) {\n        String chunkLen = Integer.toString(bb.remaining(), 16) + \"\\r\\n\";\n        bb.addFirst(ByteBuffer.wrap(chunkLen.getBytes()));\n        bb.add(ByteBuffer.wrap(\"\\r\\n\".getBytes()));\n        return bb;\n    }\n\n    @Override\n    public void end() {\n        setMaxBuffer(Integer.MAX_VALUE);\n        ByteBufferList fin = new ByteBufferList();\n        write(fin);\n        setMaxBuffer(0);\n        // do NOT call through to super.end, as chunking is a framing protocol.\n        // we don't want to close the underlying transport.\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/ContentLengthFilter.java",
    "content": "package com.jeffmony.async.http.filter;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\n\npublic class ContentLengthFilter extends FilteredDataEmitter {\n    public ContentLengthFilter(long contentLength) {\n        this.contentLength = contentLength;\n    }\n\n    @Override\n    protected void report(Exception e) {\n        if (e == null && totalRead != contentLength)\n            e = new PrematureDataEndException(\"End of data reached before content length was read: \" + totalRead + \"/\" + contentLength + \" Paused: \" + isPaused());\n        super.report(e);\n    }\n\n    long contentLength;\n    long totalRead;\n    ByteBufferList transformed = new ByteBufferList();\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        assert totalRead < contentLength;\n\n        int remaining = bb.remaining();\n        long toRead = Math.min(contentLength - totalRead, remaining);\n\n        bb.get(transformed, (int)toRead);\n\n        int beforeRead = transformed.remaining();\n\n        super.onDataAvailable(emitter, transformed);\n\n        totalRead += (beforeRead - transformed.remaining());\n        transformed.get(bb);\n\n        if (totalRead == contentLength)\n            report(null);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/DataRemainingException.java",
    "content": "package com.jeffmony.async.http.filter;\n\npublic class DataRemainingException extends Exception {\n    public DataRemainingException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/GZIPInputFilter.java",
    "content": "package com.jeffmony.async.http.filter;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.PushParser;\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Locale;\nimport java.util.zip.CRC32;\nimport java.util.zip.GZIPInputStream;\nimport java.util.zip.Inflater;\n\npublic class GZIPInputFilter extends InflaterInputFilter {\n    static short peekShort(byte[] src, int offset, ByteOrder order) {\n        if (order == ByteOrder.BIG_ENDIAN) {\n            return (short) ((src[offset] << 8) | (src[offset + 1] & 0xff));\n        } else {\n            return (short) ((src[offset + 1] << 8) | (src[offset] & 0xff));\n        }\n    }\n\n    private static final int FCOMMENT = 16;\n\n    private static final int FEXTRA = 4;\n\n    private static final int FHCRC = 2;\n\n    private static final int FNAME = 8;\n\n\n    \n    public GZIPInputFilter() {\n        super(new Inflater(true));\n    }\n    \n    boolean mNeedsHeader = true;\n    protected CRC32 crc = new CRC32();\n\n    public static int unsignedToBytes(byte b) {\n        return b & 0xFF;\n    }\n    \n    @Override\n    @SuppressWarnings(\"unused\")\n    public void onDataAvailable(final DataEmitter emitter, ByteBufferList bb) {\n        if (mNeedsHeader) {\n            final PushParser parser = new PushParser(emitter);\n            parser.readByteArray(10, new PushParser.ParseCallback<byte[]>() {\n                int flags;\n                boolean hcrc;\n\n                public void parsed(byte[] header) {\n                    short magic = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN);\n                    if (magic != (short) GZIPInputStream.GZIP_MAGIC) {\n                        report(new IOException(String.format(Locale.ENGLISH, \"unknown format (magic number %x)\", magic)));\n                        emitter.setDataCallback(new DataCallback.NullDataCallback());\n                        return;\n                    }\n                    flags = header[3];\n                    hcrc = (flags & FHCRC) != 0;\n                    if (hcrc) {\n                        crc.update(header, 0, header.length);\n                    }\n                    if ((flags & FEXTRA) != 0) {\n                        parser.readByteArray(2, new PushParser.ParseCallback<byte[]>() {\n                            public void parsed(byte[] header) {\n                                if (hcrc) {\n                                    crc.update(header, 0, 2);\n                                }\n                                int length = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff;\n                                parser.readByteArray(length, new PushParser.ParseCallback<byte[]>() {\n                                    public void parsed(byte[] buf) {\n                                        if (hcrc) {\n                                            crc.update(buf, 0, buf.length);\n                                        }\n                                        next();\n                                    }\n                                });\n                            }\n                        });\n                    } else {\n                        next();\n                    }\n                }\n\n                private void next() {\n                    PushParser parser = new PushParser(emitter);\n                    DataCallback summer = new DataCallback() {\n                        @Override\n                        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                            if (hcrc) {\n                                while (bb.size() > 0) {\n                                    ByteBuffer b = bb.remove();\n                                    crc.update(b.array(), b.arrayOffset() + b.position(), b.remaining());\n                                    ByteBufferList.reclaim(b);\n                                }\n                            }\n                            bb.recycle();\n                            done();\n                        }\n                    };\n                    if ((flags & FNAME) != 0) {\n                        parser.until((byte) 0, summer);\n                        return;\n                    }\n                    if ((flags & FCOMMENT) != 0) {\n                        parser.until((byte) 0, summer);\n                        return;\n                    }\n\n                    done();\n                }\n\n                private void done() {\n                    if (hcrc) {\n                        parser.readByteArray(2, new PushParser.ParseCallback<byte[]>() {\n                            public void parsed(byte[] header) {\n                                short crc16 = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN);\n                                if ((short) crc.getValue() != crc16) {\n                                    report(new IOException(\"CRC mismatch\"));\n                                    return;\n                                }\n                                crc.reset();\n                                mNeedsHeader = false;\n                                setDataEmitter(emitter);\n//                            emitter.setDataCallback(GZIPInputFilter.this);\n                            }\n                        });\n                    } else {\n                        mNeedsHeader = false;\n                        setDataEmitter(emitter);\n                    }\n                }\n            });\n        }\n        else {\n            super.onDataAvailable(emitter, bb);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/InflaterInputFilter.java",
    "content": "package com.jeffmony.async.http.filter;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\nimport com.jeffmony.async.Util;\n\nimport java.nio.ByteBuffer;\nimport java.util.zip.Inflater;\n\npublic class InflaterInputFilter extends FilteredDataEmitter {\n    private Inflater mInflater;\n\n    @Override\n    protected void report(Exception e) {\n        mInflater.end();\n        if (e != null && mInflater.getRemaining() > 0) {\n            e = new DataRemainingException(\"data still remaining in inflater\", e);\n        }\n        super.report(e);\n    }\n\n    ByteBufferList transformed = new ByteBufferList();\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        try {\n            ByteBuffer output = ByteBufferList.obtain(bb.remaining() * 2);\n            int totalRead = 0;\n            while (bb.size() > 0) {\n                ByteBuffer b = bb.remove();\n                if (b.hasRemaining()) {\n                    totalRead =+ b.remaining();\n                    mInflater.setInput(b.array(), b.arrayOffset() + b.position(), b.remaining());\n                    do {\n                        int inflated = mInflater.inflate(output.array(), output.arrayOffset() + output.position(), output.remaining());\n                        output.position(output.position() + inflated);\n                        if (!output.hasRemaining()) {\n                            output.flip();\n                            transformed.add(output);\n                            assert totalRead != 0;\n                            int newSize = output.capacity() * 2;\n                            output = ByteBufferList.obtain(newSize);\n                        }\n                    }\n                    while (!mInflater.needsInput() && !mInflater.finished());\n                }\n                ByteBufferList.reclaim(b);\n            }\n            output.flip();\n            transformed.add(output);\n\n            Util.emitAllData(this, transformed);\n        }\n        catch (Exception ex) {\n            report(ex);\n        }\n    }\n\n    public InflaterInputFilter() {\n        this(new Inflater());\n    }\n\n    public InflaterInputFilter(Inflater inflater) {\n        mInflater = inflater;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/filter/PrematureDataEndException.java",
    "content": "package com.jeffmony.async.http.filter;\n\npublic class PrematureDataEndException extends Exception {\n    public PrematureDataEndException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpRequestBodyProvider.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\npublic interface AsyncHttpRequestBodyProvider {\n    AsyncHttpRequestBody getBody(Headers headers);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServer.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.util.Log;\n\nimport com.jeffmony.async.AsyncSSLSocket;\nimport com.jeffmony.async.AsyncSSLSocketWrapper;\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncServerSocket;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.ListenCallback;\nimport com.jeffmony.async.callback.ValueCallback;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.HttpUtil;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.WebSocket;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.Hashtable;\n\nimport javax.net.ssl.SSLContext;\n\n@TargetApi(Build.VERSION_CODES.ECLAIR)\npublic class AsyncHttpServer extends AsyncHttpServerRouter {\n    ArrayList<AsyncServerSocket> mListeners = new ArrayList<AsyncServerSocket>();\n    public void stop() {\n        if (mListeners != null) {\n            for (AsyncServerSocket listener: mListeners) {\n                listener.stop();\n            }\n        }\n    }\n    \n    protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n        return false;\n    }\n\n    protected void onResponseCompleted(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n\n    }\n\n    protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n        if (callback != null) {\n            try {\n                callback.onRequest(request, response);\n            }\n            catch (Exception e) {\n                Log.e(\"AsyncHttpServer\", \"request callback raised uncaught exception. Catching versus crashing process\", e);\n                response.code(500);\n                response.end();\n            }\n        }\n    }\n\n    protected boolean isKeepAlive(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n        return HttpUtil.isKeepAlive(response.getHttpVersion(), request.getHeaders());\n    }\n\n    protected AsyncHttpRequestBody onUnknownBody(Headers headers) {\n        return new UnknownRequestBody(headers.get(\"Content-Type\"));\n    }\n\n    protected boolean isSwitchingProtocols(AsyncHttpServerResponse res) {\n        return res.code() == 101;\n    }\n\n    ListenCallback mListenCallback = new ListenCallback() {\n        @Override\n        public void onAccepted(final AsyncSocket socket) {\n            final AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() {\n                AsyncHttpServerRequestImpl self = this;\n                HttpServerRequestCallback requestCallback;\n                String fullPath;\n                String path;\n                boolean responseComplete;\n                boolean requestComplete;\n                AsyncHttpServerResponseImpl res;\n                boolean hasContinued;\n                boolean handled;\n\n                final Runnable onFinally = new Runnable() {\n                    @Override\n                    public void run() {\n                        Log.i(\"HTTP\", \"Done\");\n                    }\n                };\n\n                final ValueCallback<Exception> onException = new ValueCallback<Exception>() {\n                    @Override\n                    public void onResult(Exception value) {\n                        Log.e(\"HTTP\", \"exception\", value);\n                    }\n                };\n\n                void onRequest() {\n                    AsyncHttpServer.this.onRequest(requestCallback, this, res);\n                }\n\n                @Override\n                protected AsyncHttpRequestBody onBody(Headers headers) {\n                    String statusLine = getStatusLine();\n                    String[] parts = statusLine.split(\" \");\n                    fullPath = parts[1];\n                    path = URLDecoder.decode(fullPath.split(\"\\\\?\")[0]);\n                    method = parts[0];\n                    RouteMatch route = route(method, path);\n                    if (route == null)\n                        return null;\n\n                    matcher = route.matcher;\n                    requestCallback = route.callback;\n\n                    if (route.bodyCallback == null)\n                        return null;\n                    return route.bodyCallback.getBody(headers);\n                }\n\n                @Override\n                protected AsyncHttpRequestBody onUnknownBody(Headers headers) {\n                    return AsyncHttpServer.this.onUnknownBody(headers);\n                }\n\n                @Override\n                protected void onHeadersReceived() {\n                    Headers headers = getHeaders();\n\n                    // should the negotiation of 100 continue be here, or in the request impl?\n                    // probably here, so AsyncResponse can negotiate a 100 continue.\n                    if (!hasContinued && \"100-continue\".equals(headers.get(\"Expect\"))) {\n                        pause();\n//                        System.out.println(\"continuing...\");\n                        Util.writeAll(mSocket, \"HTTP/1.1 100 Continue\\r\\n\\r\\n\".getBytes(), new CompletedCallback() {\n                            @Override\n                            public void onCompleted(Exception ex) {\n                                resume();\n                                if (ex != null) {\n                                    report(ex);\n                                    return;\n                                }\n                                hasContinued = true;\n                                onHeadersReceived();\n                            }\n                        });\n                        return;\n                    }\n//                    System.out.println(headers.toHeaderString());\n                    \n                    res = new AsyncHttpServerResponseImpl(socket, this) {\n                        @Override\n                        protected void report(Exception e) {\n                            super.report(e);\n                            if (e != null) {\n                                socket.setDataCallback(new NullDataCallback());\n                                socket.setEndCallback(new NullCompletedCallback());\n                                socket.close();\n                            }\n                        }\n\n                        @Override\n                        protected void onEnd() {\n                            responseComplete = true;\n                            super.onEnd();\n                            mSocket.setEndCallback(null);\n\n                            onResponseCompleted(getRequest(), res);\n\n                            // reuse the socket for a subsequent request.\n                            handleOnCompleted();\n                        }\n                    };\n\n                    handled = AsyncHttpServer.this.onRequest(this, res);\n                    if (handled)\n                        return;\n\n                    if (requestCallback == null) {\n                        res.code(404);\n                        res.end();\n                        return;\n                    }\n\n                    if (!getBody().readFullyOnRequest() || requestComplete)\n                        onRequest();\n                }\n\n                @Override\n                public void onCompleted(Exception e) {\n                    if (isSwitchingProtocols(res))\n                        return;\n\n                    requestComplete = true;\n                    super.onCompleted(e);\n                    // no http pipelining, gc trashing if the socket dies\n                    // while the request is being sent and is paused or something\n                    mSocket.setDataCallback(new NullDataCallback() {\n                        @Override\n                        public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n                            super.onDataAvailable(emitter, bb);\n                            mSocket.close();\n                        }\n                    });\n\n                    if (e != null) {\n                        mSocket.close();\n                        return;\n                    }\n\n                    handleOnCompleted();\n\n                    if (getBody().readFullyOnRequest() && !handled) {\n                        onRequest();\n                    }\n                }\n                \n                private void handleOnCompleted() {\n                    // response may complete before request. the request may have a body, and\n                    // the response may be sent before it is fully sent.\n\n                    // if the protocol was switched off http, abandon the socket,\n                    // otherwise attempt to recycle it.\n                    if (requestComplete && responseComplete && !isSwitchingProtocols(res)) {\n                        if (isKeepAlive(self, res)) {\n                            onAccepted(socket);\n                        }\n                        else {\n                            socket.close();\n                        }\n                    }\n                }\n\n                @Override\n                public String getPath() {\n                    return path;\n                }\n\n                @Override\n                public Multimap getQuery() {\n                    String[] parts = fullPath.split(\"\\\\?\", 2);\n                    if (parts.length < 2)\n                        return new Multimap();\n                    return Multimap.parseQuery(parts[1]);\n                }\n\n                @Override\n                public String getUrl() {\n                    return fullPath;\n                }\n            };\n            req.setSocket(socket);\n            socket.resume();\n        }\n\n        @Override\n        public void onCompleted(Exception error) {\n            report(error);\n        }\n\n        @Override\n        public void onListening(AsyncServerSocket socket) {\n            mListeners.add(socket);\n        }\n    };\n\n    public AsyncServerSocket listen(AsyncServer server, int port) {\n        return server.listen(null, port, mListenCallback);\n    }\n\n    private void report(Exception ex) {\n        if (mCompletedCallback != null)\n            mCompletedCallback.onCompleted(ex);\n    }\n    \n    public AsyncServerSocket listen(int port) {\n        return listen(AsyncServer.getDefault(), port);\n    }\n\n    public void listenSecure(final int port, final SSLContext sslContext) {\n        AsyncServer.getDefault().listen(null, port, new ListenCallback() {\n            @Override\n            public void onAccepted(AsyncSocket socket) {\n                AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false,\n                new AsyncSSLSocketWrapper.HandshakeCallback() {\n                    @Override\n                    public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {\n                        if (socket != null)\n                            mListenCallback.onAccepted(socket);\n                    }\n                });\n            }\n\n            @Override\n            public void onListening(AsyncServerSocket socket) {\n                mListenCallback.onListening(socket);\n            }\n\n            @Override\n            public void onCompleted(Exception ex) {\n                mListenCallback.onCompleted(ex);\n            }\n        });\n    }\n    \n    public ListenCallback getListenCallback() {\n        return mListenCallback;\n    }\n\n    CompletedCallback mCompletedCallback;\n    public void setErrorCallback(CompletedCallback callback) {\n        mCompletedCallback = callback;        \n    }\n\n    public CompletedCallback getErrorCallback() {\n        return mCompletedCallback;\n    }\n\n    private static Hashtable<Integer, String> mCodes = new Hashtable<Integer, String>();\n    static {\n        mCodes.put(200, \"OK\");\n        mCodes.put(202, \"Accepted\");\n        mCodes.put(206, \"Partial Content\");\n        mCodes.put(101, \"Switching Protocols\");\n        mCodes.put(301, \"Moved Permanently\");\n        mCodes.put(302, \"Found\");\n        mCodes.put(304, \"Not Modified\");\n        mCodes.put(400, \"Bad Request\");\n        mCodes.put(404, \"Not Found\");\n        mCodes.put(500, \"Internal Server Error\");\n    }\n    \n    public static String getResponseCodeDescription(int code) {\n        String d = mCodes.get(code);\n        if (d == null)\n            return \"Unknown\";\n        return d;\n    }\n\n    public static interface WebSocketRequestCallback {\n        void onConnected(WebSocket webSocket, AsyncHttpServerRequest request);\n    }\n\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRequest.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\nimport java.util.Map;\nimport java.util.regex.Matcher;\n\npublic interface AsyncHttpServerRequest extends DataEmitter {\n    Headers getHeaders();\n    Matcher getMatcher();\n    void setMatcher(Matcher matcher);\n    <T extends AsyncHttpRequestBody> T getBody();\n    AsyncSocket getSocket();\n    String getPath();\n    Multimap getQuery();\n    String getMethod();\n    String getUrl();\n\n    String get(String name);\n    Map<String, Object> getState();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRequestImpl.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\n\nimport com.jeffmony.async.LineEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.HttpUtil;\nimport com.jeffmony.async.http.Multimap;\nimport com.jeffmony.async.http.Protocol;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\nimport java.io.IOException;\nimport java.util.HashMap;\n\npublic abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter implements AsyncHttpServerRequest, CompletedCallback {\n    private String statusLine;\n    private Headers mRawHeaders = new Headers();\n    AsyncSocket mSocket;\n    private HashMap<String, Object> state = new HashMap<>();\n\n    @Override\n    public HashMap<String, Object> getState() {\n        return state;\n    }\n\n    public String getStatusLine() {\n        return statusLine;\n    }\n\n    private CompletedCallback mReporter = new CompletedCallback() {\n        @Override\n        public void onCompleted(Exception error) {\n            AsyncHttpServerRequestImpl.this.onCompleted(error);\n        }\n    };\n\n    @Override\n    public void onCompleted(Exception e) {\n//        if (mBody != null)\n//            mBody.onCompleted(e);\n        report(e);\n    }\n\n    abstract protected void onHeadersReceived();\n    \n    protected void onNotHttp() {\n        System.out.println(\"not http!\");\n    }\n\n    protected AsyncHttpRequestBody onUnknownBody(Headers headers) {\n        return null;\n    }\n    protected AsyncHttpRequestBody onBody(Headers headers) {\n        return null;\n    }\n\n    \n    LineEmitter.StringCallback mHeaderCallback = new LineEmitter.StringCallback() {\n        @Override\n        public void onStringAvailable(String s) {\n            if (statusLine == null) {\n                statusLine = s;\n                if (!statusLine.contains(\"HTTP/\")) {\n                    onNotHttp();\n                    mSocket.setDataCallback(new NullDataCallback());\n                    report(new IOException(\"data/header received was not not http\"));\n                }\n\n                return;\n            }\n            if (!\"\\r\".equals(s)){\n                mRawHeaders.addLine(s);\n                return;\n            }\n\n            DataEmitter emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.HTTP_1_1, mRawHeaders, true);\n            mBody = onBody(mRawHeaders);\n            if (mBody == null) {\n                mBody = HttpUtil.getBody(emitter, mReporter, mRawHeaders);\n                if (mBody == null) {\n                    mBody = onUnknownBody(mRawHeaders);\n                    if (mBody == null)\n                        mBody = new UnknownRequestBody(mRawHeaders.get(\"Content-Type\"));\n                }\n            }\n            mBody.parse(emitter, mReporter);\n            onHeadersReceived();\n        }\n    };\n\n    String method;\n    @Override\n    public String getMethod() {\n        return method;\n    }\n    \n    void setSocket(AsyncSocket socket) {\n        mSocket = socket;\n\n        LineEmitter liner = new LineEmitter();\n        mSocket.setDataCallback(liner);\n        liner.setLineCallback(mHeaderCallback);\n        mSocket.setEndCallback(new NullCompletedCallback());\n    }\n    \n    @Override\n    public AsyncSocket getSocket() {\n        return mSocket;\n    }\n\n    @Override\n    public Headers getHeaders() {\n        return mRawHeaders;\n    }\n\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        mSocket.setDataCallback(callback);\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return mSocket.getDataCallback();\n    }\n\n    @Override\n    public boolean isChunked() {\n        return mSocket.isChunked();\n    }\n\n    AsyncHttpRequestBody mBody;\n    @Override\n    public AsyncHttpRequestBody getBody() {\n        return mBody;\n    }\n\n    @Override\n    public void pause() {\n        mSocket.pause();\n    }\n\n    @Override\n    public void resume() {\n        mSocket.resume();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return mSocket.isPaused();\n    }\n\n    @Override\n    public String toString() {\n        if (mRawHeaders == null)\n            return super.toString();\n        return mRawHeaders.toPrefixString(statusLine);\n    }\n\n    @Override\n    public String get(String name) {\n        Multimap query = getQuery();\n        String ret = query.getString(name);\n        if (ret != null)\n            return ret;\n        AsyncHttpRequestBody body = getBody();\n        Object bodyObject = body.get();\n        if (bodyObject instanceof Multimap) {\n            Multimap map = (Multimap)bodyObject;\n            return map.getString(name);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerResponse.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.http.AsyncHttpResponse;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.parser.AsyncParser;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\n\npublic interface AsyncHttpServerResponse extends DataSink, CompletedCallback {\n    void end();\n    void send(String contentType, byte[] bytes);\n    void send(String contentType, ByteBufferList bb);\n    void send(String contentType, ByteBuffer bb);\n    void send(String contentType, String string);\n    void send(String string);\n    void send(JSONObject json);\n    void send(JSONArray jsonArray);\n    void sendFile(File file);\n    void sendStream(InputStream inputStream, long totalLength);\n    <T> void sendBody(AsyncParser<T> body, T value);\n    AsyncHttpServerResponse code(int code);\n    int code();\n    Headers getHeaders();\n    void writeHead();\n    void setContentType(String contentType);\n    void redirect(String location);\n    AsyncHttpServerRequest getRequest();\n    String getHttpVersion();\n    void setHttpVersion(String httpVersion);\n\n    // NOT FINAL\n    void proxy(AsyncHttpResponse response);\n\n    /**\n     * Alias for end. Used with CompletedEmitters\n     */\n    void onCompleted(Exception ex);\n    AsyncSocket getSocket();\n    void setSocket(AsyncSocket socket);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerResponseImpl.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport android.text.TextUtils;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncSocket;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.callback.WritableCallback;\nimport com.jeffmony.async.http.AsyncHttpHead;\nimport com.jeffmony.async.http.AsyncHttpResponse;\nimport com.jeffmony.async.http.Headers;\nimport com.jeffmony.async.http.HttpUtil;\nimport com.jeffmony.async.http.Protocol;\nimport com.jeffmony.async.http.filter.ChunkedOutputFilter;\nimport com.jeffmony.async.parser.AsyncParser;\nimport com.jeffmony.async.util.StreamUtility;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.BufferedInputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.InputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.ByteBuffer;\nimport java.util.Locale;\n\npublic class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse {\n    private Headers mRawHeaders = new Headers();\n    private long mContentLength = -1;\n\n    @Override\n    public Headers getHeaders() {\n        return mRawHeaders;\n    }\n    \n    public AsyncSocket getSocket() {\n        return mSocket;\n    }\n\n    @Override\n    public void setSocket(AsyncSocket socket) {\n        mSocket = socket;\n    }\n\n    AsyncSocket mSocket;\n    AsyncHttpServerRequestImpl mRequest;\n    AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) {\n        mSocket = socket;\n        mRequest = req;\n        if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, req.getHeaders()))\n            mRawHeaders.set(\"Connection\", \"Keep-Alive\");\n    }\n\n    @Override\n    public AsyncHttpServerRequest getRequest() {\n        return mRequest;\n    }\n\n    @Override\n    public void write(ByteBufferList bb) {\n        // order is important here...\n        assert !mEnded;\n        // do the header write... this will call onWritable, which may be reentrant\n        if (!headWritten)\n            initFirstWrite();\n\n        // now check to see if the list is empty. reentrancy may cause it to empty itself.\n        if (bb.remaining() == 0)\n            return;\n\n        // null sink means that the header has not finished writing\n        if (mSink == null)\n            return;\n\n        // can successfully write!\n        mSink.write(bb);\n    }\n\n    boolean headWritten = false;\n    DataSink mSink;\n    void initFirstWrite() {\n        if (headWritten)\n            return;\n\n        headWritten = true;\n\n        final boolean isChunked;\n        String currentEncoding = mRawHeaders.get(\"Transfer-Encoding\");\n        if (\"\".equals(currentEncoding))\n            mRawHeaders.removeAll(\"Transfer-Encoding\");\n        boolean canUseChunked = (\"Chunked\".equalsIgnoreCase(currentEncoding) || currentEncoding == null)\n        && !\"close\".equalsIgnoreCase(mRawHeaders.get(\"Connection\"));\n        if (mContentLength < 0) {\n            String contentLength = mRawHeaders.get(\"Content-Length\");\n            if (!TextUtils.isEmpty(contentLength))\n                mContentLength = Long.valueOf(contentLength);\n        }\n        if (mContentLength < 0 && canUseChunked) {\n            mRawHeaders.set(\"Transfer-Encoding\", \"Chunked\");\n            isChunked = true;\n        }\n        else {\n            isChunked = false;\n        }\n\n        String statusLine = String.format(Locale.ENGLISH, \"%s %s %s\", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code));\n        String rh = mRawHeaders.toPrefixString(statusLine);\n\n        Util.writeAll(mSocket, rh.getBytes(), ex -> {\n            if (ex != null) {\n                report(ex);\n                return;\n            }\n            if (isChunked) {\n                ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket);\n                chunked.setMaxBuffer(0);\n                mSink = chunked;\n            }\n            else {\n                mSink = mSocket;\n            }\n\n            mSink.setClosedCallback(closedCallback);\n            closedCallback = null;\n            mSink.setWriteableCallback(writable);\n            writable = null;\n            if (ended) {\n                // the response ended while headers were written\n                end();\n                return;\n            }\n            getServer().post(() -> {\n                WritableCallback wb = getWriteableCallback();\n                if (wb != null)\n                    wb.onWriteable();\n            });\n        });\n    }\n\n    WritableCallback writable;\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        if (mSink != null)\n            mSink.setWriteableCallback(handler);\n        else\n            writable = handler;\n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        if (mSink != null)\n            return mSink.getWriteableCallback();\n        return writable;\n    }\n\n    boolean ended;\n    @Override\n    public void end() {\n        if (ended)\n            return;\n        ended = true;\n        if (headWritten && mSink == null) {\n            // header is in the process of being written... bail out.\n            // end will be called again after finished.\n            return;\n        }\n        if (!headWritten) {\n            // end was called, and no head or body was yet written,\n            // so strip the transfer encoding as that is superfluous.\n            mRawHeaders.remove(\"Transfer-Encoding\");\n        }\n        if (mSink instanceof ChunkedOutputFilter) {\n            // this filter won't close the socket underneath.\n            mSink.end();\n        }\n        else if (!headWritten) {\n            if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD))\n                send(\"text/html\", \"\");\n            else {\n                writeHead();\n                onEnd();\n            }\n        }\n        else {\n            onEnd();\n        }\n    }\n\n    @Override\n    public void writeHead() {\n        initFirstWrite();\n    }\n\n    @Override\n    public void setContentType(String contentType) {\n        mRawHeaders.set(\"Content-Type\", contentType);\n    }\n\n    @Override\n    public void send(final String contentType, final byte[] bytes) {\n        send(contentType, new ByteBufferList(bytes));\n    }\n\n    @Override\n    public <T> void sendBody(AsyncParser<T> body, T value) {\n        mRawHeaders.set(\"Content-Type\", body.getMime());\n        body.write(this, value, ex -> end());\n    }\n\n    @Override\n    public void send(String contentType, ByteBuffer bb) {\n        send(contentType, new ByteBufferList(bb));\n    }\n\n    @Override\n    public void send(String contentType, ByteBufferList bb) {\n        getServer().post(() -> {\n            mContentLength = bb.remaining();\n            mRawHeaders.set(\"Content-Length\", Long.toString(mContentLength));\n            if (contentType != null)\n                mRawHeaders.set(\"Content-Type\", contentType);\n\n            Util.writeAll(AsyncHttpServerResponseImpl.this, bb, ex -> onEnd());\n        });\n    }\n\n    @Override\n    public void send(String contentType, final String string) {\n        try {\n            send(contentType, string.getBytes(\"UTF-8\"));\n        }\n        catch (UnsupportedEncodingException e) {\n            throw new AssertionError(e);\n        }\n    }\n    \n    boolean mEnded;\n    protected void onEnd() {\n        mEnded = true;\n    }\n    \n    protected void report(Exception e) {\n    }\n\n\n    @Override\n    public void send(String string) {\n        String contentType = mRawHeaders.get(\"Content-Type\");\n        if (contentType == null)\n            contentType = \"text/html; charset=utf-8\";\n        send(contentType, string);\n    }\n\n    @Override\n    public void send(JSONObject json) {\n        send(\"application/json; charset=utf-8\", json.toString());\n    }\n\n    @Override\n    public void send(JSONArray jsonArray) {\n        send(\"application/json; charset=utf-8\", jsonArray.toString());\n    }\n\n    @Override\n    public void sendStream(final InputStream inputStream, long totalLength) {\n        long start = 0;\n        long end = totalLength - 1;\n\n        String range = mRequest.getHeaders().get(\"Range\");\n        if (range != null) {\n            String[] parts = range.split(\"=\");\n            if (parts.length != 2 || !\"bytes\".equals(parts[0])) {\n                // Requested range not satisfiable\n                code(416);\n                end();\n                return;\n            }\n\n            parts = parts[1].split(\"-\");\n            try {\n                if (parts.length > 2)\n                    throw new MalformedRangeException();\n                if (!TextUtils.isEmpty(parts[0]))\n                    start = Long.parseLong(parts[0]);\n                if (parts.length == 2 && !TextUtils.isEmpty(parts[1]))\n                    end = Long.parseLong(parts[1]);\n                else\n                    end = totalLength - 1;\n\n                code(206);\n                getHeaders().set(\"Content-Range\", String.format(Locale.ENGLISH, \"bytes %d-%d/%d\", start, end, totalLength));\n            }\n            catch (Exception e) {\n                code(416);\n                end();\n                return;\n            }\n        }\n        try {\n            if (start != inputStream.skip(start))\n                throw new StreamSkipException(\"skip failed to skip requested amount\");\n            mContentLength = end - start + 1;\n            mRawHeaders.set(\"Content-Length\", String.valueOf(mContentLength));\n            mRawHeaders.set(\"Accept-Ranges\", \"bytes\");\n            if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) {\n                writeHead();\n                onEnd();\n                return;\n            }\n            if (mContentLength == 0) {\n                writeHead();\n                StreamUtility.closeQuietly(inputStream);\n                onEnd();\n                return;\n            }\n            getServer().post(() ->\n                    Util.pump(inputStream, mContentLength, AsyncHttpServerResponseImpl.this,\n                            ex -> {\n                                StreamUtility.closeQuietly(inputStream);\n                                onEnd();\n                            }));\n        }\n        catch (Exception e) {\n            code(500);\n            end();\n        }\n    }\n\n    @Override\n    public void sendFile(File file) {\n        try {\n            if (mRawHeaders.get(\"Content-Type\") == null)\n                mRawHeaders.set(\"Content-Type\", AsyncHttpServer.getContentType(file.getAbsolutePath()));\n            FileInputStream fin = new FileInputStream(file);\n            sendStream(new BufferedInputStream(fin, 64000), file.length());\n        }\n        catch (FileNotFoundException e) {\n            code(404);\n            end();\n        }\n    }\n\n    @Override\n    public void proxy(final AsyncHttpResponse remoteResponse) {\n        code(remoteResponse.code());\n        remoteResponse.headers().removeAll(\"Transfer-Encoding\");\n        remoteResponse.headers().removeAll(\"Content-Encoding\");\n        remoteResponse.headers().removeAll(\"Connection\");\n        getHeaders().addAll(remoteResponse.headers());\n        // TODO: remove?\n        remoteResponse.headers().set(\"Connection\", \"close\");\n        Util.pump(remoteResponse, this, ex -> {\n            remoteResponse.setEndCallback(new NullCompletedCallback());\n            remoteResponse.setDataCallback(new DataCallback.NullDataCallback());\n            end();\n        });\n    }\n\n    int code = 200;\n    @Override\n    public AsyncHttpServerResponse code(int code) {\n        this.code = code;\n        return this;\n    }\n\n    @Override\n    public int code() {\n        return code;\n    }\n\n    @Override\n    public void redirect(String location) {\n        code(302);\n        mRawHeaders.set(\"Location\", location);\n        end();\n    }\n\n    String httpVersion = \"HTTP/1.1\";\n    @Override\n    public String getHttpVersion() {\n        return httpVersion;\n    }\n\n    @Override\n    public void setHttpVersion(String httpVersion) {\n        this.httpVersion = httpVersion;\n    }\n\n    @Override\n    public void onCompleted(Exception ex) {\n        end();\n    }\n\n    @Override\n    public boolean isOpen() {\n        if (mSink != null)\n            return mSink.isOpen();\n        return mSocket.isOpen();\n    }\n\n    CompletedCallback closedCallback;\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        if (mSink != null)\n            mSink.setClosedCallback(handler);\n        else\n            closedCallback = handler;\n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        if (mSink != null)\n            return mSink.getClosedCallback();\n        return closedCallback;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return mSocket.getServer();\n    }\n\n    @Override\n    public String toString() {\n        if (mRawHeaders == null)\n            return super.toString();\n        String statusLine = String.format(Locale.ENGLISH, \"%s %s %s\", httpVersion, code, AsyncHttpServer.getResponseCodeDescription(code));\n        return mRawHeaders.toPrefixString(statusLine);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncHttpServerRouter.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport android.content.Context;\nimport android.content.res.AssetManager;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.SimpleFuture;\nimport com.jeffmony.async.http.AsyncHttpGet;\nimport com.jeffmony.async.http.AsyncHttpHead;\nimport com.jeffmony.async.http.AsyncHttpPost;\nimport com.jeffmony.async.http.WebSocket;\nimport com.jeffmony.async.http.WebSocketImpl;\nimport com.jeffmony.async.util.StreamUtility;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Hashtable;\nimport java.util.jar.Manifest;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\npublic class AsyncHttpServerRouter implements RouteMatcher {\n\n    private static class RouteInfo {\n        String method;\n        Pattern regex;\n        HttpServerRequestCallback callback;\n        AsyncHttpRequestBodyProvider bodyCallback;\n    }\n\n    final ArrayList<RouteInfo> routes = new ArrayList<>();\n\n    public void removeAction(String action, String regex) {\n        for (int i = 0; i < routes.size(); i++) {\n            RouteInfo p = routes.get(i);\n            if (TextUtils.equals(p.method, action) && regex.equals(p.regex.toString())) {\n                routes.remove(i);\n                return;\n            }\n        }\n    }\n\n    public void addAction(String action, String regex, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) {\n        RouteInfo p = new RouteInfo();\n        p.regex = Pattern.compile(\"^\" + regex);\n        p.callback = callback;\n        p.method = action;\n        p.bodyCallback = bodyCallback;\n\n        synchronized (routes) {\n            routes.add(p);\n        }\n    }\n\n    public void addAction(String action, String regex, HttpServerRequestCallback callback) {\n        addAction(action, regex, callback, null);\n    }\n\n    public void websocket(String regex, final AsyncHttpServer.WebSocketRequestCallback callback) {\n        websocket(regex, null, callback);\n    }\n\n    static public WebSocket checkWebSocketUpgrade(final String protocol, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {\n        boolean hasUpgrade = false;\n        String connection = request.getHeaders().get(\"Connection\");\n        if (connection != null) {\n            String[] connections = connection.split(\",\");\n            for (String c: connections) {\n                if (\"Upgrade\".equalsIgnoreCase(c.trim())) {\n                    hasUpgrade = true;\n                    break;\n                }\n            }\n        }\n        if (!\"websocket\".equalsIgnoreCase(request.getHeaders().get(\"Upgrade\")) || !hasUpgrade) {\n            return null;\n        }\n        String peerProtocol = request.getHeaders().get(\"Sec-WebSocket-Protocol\");\n        if (!TextUtils.equals(protocol, peerProtocol)) {\n            return null;\n        }\n        return new WebSocketImpl(request, response);\n    }\n\n    public void websocket(String regex, final String protocol, final AsyncHttpServer.WebSocketRequestCallback callback) {\n        get(regex, (request, response) -> {\n            WebSocket webSocket = checkWebSocketUpgrade(protocol, request, response);\n            if (webSocket == null) {\n                response.code(404);\n                response.end();\n                return;\n            }\n\n            callback.onConnected(webSocket, request);\n        });\n    }\n\n    public void get(String regex, HttpServerRequestCallback callback) {\n        addAction(AsyncHttpGet.METHOD, regex, callback);\n    }\n\n    public void post(String regex, HttpServerRequestCallback callback) {\n        addAction(AsyncHttpPost.METHOD, regex, callback);\n    }\n\n    public static class Asset {\n        public Asset(int available, InputStream inputStream, String path) {\n            this.available = available;\n            this.inputStream = inputStream;\n            this.path = path;\n        }\n\n        public int available;\n        public InputStream inputStream;\n        public String path;\n    }\n\n    public static Asset getAssetStream(final Context context, String asset) {\n        return getAssetStream(context.getAssets(), asset);\n    }\n\n    public static Asset getAssetStream(AssetManager am, String asset) {\n        try {\n            InputStream is = am.open(asset);\n            return new Asset(is.available(), is, asset);\n        }\n        catch (IOException e) {\n            final String[] extensions = new String[] { \"/index.htm\", \"/index.html\", \"index.htm\", \"index.html\", \".htm\", \".html\" };\n            for (String ext: extensions) {\n                try {\n                    InputStream is = am.open(asset + ext);\n                    return new Asset(is.available(), is, asset + ext);\n                }\n                catch (IOException ex) {\n                }\n            }\n            return null;\n        }\n    }\n\n    static Hashtable<String, String> mContentTypes = new Hashtable<String, String>();\n    {\n        mContentTypes.put(\"js\", \"application/javascript\");\n        mContentTypes.put(\"json\", \"application/json\");\n        mContentTypes.put(\"png\", \"image/png\");\n        mContentTypes.put(\"jpg\", \"image/jpeg\");\n        mContentTypes.put(\"jpeg\", \"image/jpeg\");\n        mContentTypes.put(\"html\", \"text/html\");\n        mContentTypes.put(\"css\", \"text/css\");\n        mContentTypes.put(\"mp4\", \"video/mp4\");\n        mContentTypes.put(\"mov\", \"video/quicktime\");\n        mContentTypes.put(\"wmv\", \"video/x-ms-wmv\");\n        mContentTypes.put(\"txt\", \"text/plain\");\n    }\n\n    public static String getContentType(String path) {\n        return tryGetContentType(path);\n    }\n\n    public static String tryGetContentType(String path) {\n        int index = path.lastIndexOf(\".\");\n        if (index != -1) {\n            String e = path.substring(index + 1);\n            String ct = mContentTypes.get(e);\n            if (ct != null)\n                return ct;\n        }\n        return null;\n    }\n\n    static Hashtable<String, Future<Manifest>> AppManifests = new Hashtable<>();\n    static synchronized Manifest ensureManifest(Context context) {\n        Future<Manifest> future = AppManifests.get(context.getPackageName());\n        if (future != null)\n            return future.tryGet();\n\n        ZipFile zip = null;\n        SimpleFuture<Manifest> result = new SimpleFuture<>();\n        try {\n            zip = new ZipFile(context.getPackageResourcePath());\n            ZipEntry entry = zip.getEntry(\"META-INF/MANIFEST.MF\");\n            Manifest manifest = new Manifest(zip.getInputStream(entry));\n            result.setComplete(manifest);\n            return manifest;\n        }\n        catch (Exception e) {\n            result.setComplete(e);\n            return null;\n        }\n        finally {\n            StreamUtility.closeQuietly(zip);\n            AppManifests.put(context.getPackageName(), result);\n        }\n    }\n\n    static boolean isClientCached(Context context, AsyncHttpServerRequest request, AsyncHttpServerResponse response, String assetFileName) {\n        Manifest manifest = ensureManifest(context);\n        if (manifest == null)\n            return false;\n\n        try {\n            String digest = manifest.getEntries().get(\"assets/\" + assetFileName).getValue(\"SHA-256-Digest\");\n            if (TextUtils.isEmpty(digest))\n                return false;\n\n            String etag = String.format(\"\\\"%s\\\"\", digest);\n            response.getHeaders().set(\"ETag\", etag);\n            String ifNoneMatch = request.getHeaders().get(\"If-None-Match\");\n            return TextUtils.equals(ifNoneMatch, etag);\n        }\n        catch (Exception e) {\n            Log.w(AsyncHttpServerRouter.class.getSimpleName(), \"Error getting ETag for apk asset\", e);\n            return false;\n        }\n    }\n\n    public void directory(Context context, String regex, final String assetPath) {\n        AssetManager am = context.getAssets();\n        addAction(AsyncHttpGet.METHOD, regex, (request, response) -> {\n            String path = request.getMatcher().replaceAll(\"\");\n            Asset pair = getAssetStream(am, assetPath + path);\n            if (pair == null || pair.inputStream == null) {\n                response.code(404);\n                response.end();\n                return;\n            }\n\n            if (isClientCached(context, request, response, pair.path)) {\n                StreamUtility.closeQuietly(pair.inputStream);\n                response.code(304);\n                response.end();\n                return;\n            }\n\n            response.getHeaders().set(\"Content-Length\", String.valueOf(pair.available));\n            response.getHeaders().add(\"Content-Type\", getContentType(pair.path));\n\n            response.code(200);\n            Util.pump(pair.inputStream, pair.available, response, ex -> {\n                response.end();\n                StreamUtility.closeQuietly(pair.inputStream);\n            });\n        });\n        addAction(AsyncHttpHead.METHOD, regex, (request, response) -> {\n            String path = request.getMatcher().replaceAll(\"\");\n            Asset pair = getAssetStream(am, assetPath + path);\n            if (pair == null || pair.inputStream == null) {\n                response.code(404);\n                response.end();\n                return;\n            }\n            StreamUtility.closeQuietly(pair.inputStream);\n\n            if (isClientCached(context, request, response, pair.path)) {\n                response.code(304);\n            }\n            else\n            {\n                response.getHeaders().set(\"Content-Length\", String.valueOf(pair.available));\n                response.getHeaders().add(\"Content-Type\", getContentType(pair.path));\n                response.code(200);\n            }\n\n            response.end();\n        });\n    }\n\n    public void directory(String regex, final File directory) {\n        directory(regex, directory, false);\n    }\n\n    public void directory(String regex, final File directory, final boolean list) {\n        assert directory.isDirectory();\n        addAction(AsyncHttpGet.METHOD, regex, new HttpServerRequestCallback() {\n            @Override\n            public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {\n                String path = request.getMatcher().replaceAll(\"\");\n                File file = new File(directory, path);\n\n                if (file.isDirectory() && list) {\n                    ArrayList<File> dirs = new ArrayList<File>();\n                    ArrayList<File> files = new ArrayList<File>();\n                    for (File f: file.listFiles()) {\n                        if (f.isDirectory())\n                            dirs.add(f);\n                        else\n                            files.add(f);\n                    }\n\n                    Comparator<File> c = new Comparator<File>() {\n                        @Override\n                        public int compare(File lhs, File rhs) {\n                            return lhs.getName().compareTo(rhs.getName());\n                        }\n                    };\n\n                    Collections.sort(dirs, c);\n                    Collections.sort(files, c);\n\n                    files.addAll(0, dirs);\n                    StringBuilder builder = new StringBuilder();\n                    for (File f: files) {\n                        String p = new File(request.getPath(), f.getName()).getAbsolutePath();\n                        builder.append(String.format(\"<div><a href='%s'>%s</a></div>\", p, f.getName()));\n                    }\n                    response.send(builder.toString());\n\n                    return;\n                }\n                if (!file.isFile()) {\n                    response.code(404);\n                    response.end();\n                    return;\n                }\n                try {\n                    FileInputStream is = new FileInputStream(file);\n                    response.code(200);\n                    Util.pump(is, is.available(), response, new CompletedCallback() {\n                        @Override\n                        public void onCompleted(Exception ex) {\n                            response.end();\n                        }\n                    });\n                }\n                catch (IOException ex) {\n                    response.code(404);\n                    response.end();\n                }\n            }\n        });\n    }\n\n    public static class RouteMatch {\n        public final String method;\n        public final String path;\n        public final Matcher matcher;\n        public final HttpServerRequestCallback callback;\n        public final AsyncHttpRequestBodyProvider bodyCallback;\n\n        private RouteMatch(String method, String path, Matcher matcher, HttpServerRequestCallback callback, AsyncHttpRequestBodyProvider bodyCallback) {\n            this.method = method;\n            this.path = path;\n            this.matcher = matcher;\n            this.callback = callback;\n            this.bodyCallback = bodyCallback;\n        }\n    }\n\n    abstract class AsyncHttpServerRequestImpl extends com.jeffmony.async.http.server.AsyncHttpServerRequestImpl {\n        Matcher matcher;\n        @Override\n        public Matcher getMatcher() {\n            return matcher;\n        }\n\n        @Override\n        public void setMatcher(Matcher matcher) {\n            this.matcher = matcher;\n        }\n    }\n\n    class Callback implements HttpServerRequestCallback, RouteMatcher {\n        @Override\n        public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n            RouteMatch match = route(request.getMethod(), request.getPath());\n            if (match == null) {\n                response.code(404);\n                response.end();\n                return;\n            }\n\n            match.callback.onRequest(request, response);\n        }\n\n        @Override\n        public RouteMatch route(String method, String path) {\n            return AsyncHttpServerRouter.this.route(method, path);\n        }\n    }\n\n    private Callback callback = new Callback();\n\n    public HttpServerRequestCallback getCallback() {\n        return callback;\n    }\n\n    @Override\n    public RouteMatch route(String method, String path) {\n        synchronized (routes) {\n            for (RouteInfo p: routes) {\n                // a null method is wildcard. used for nesting routers.\n                if (!TextUtils.equals(method, p.method) && p.method != null)\n                    continue;\n                Matcher m = p.regex.matcher(path);\n                if (m.matches()) {\n                    if (p.callback instanceof RouteMatcher) {\n                        String subPath = m.group(1);\n                        return ((RouteMatcher)p.callback).route(method, subPath);\n                    }\n                    return new RouteMatch(method, path, m, p.callback, p.bodyCallback);\n                }\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/AsyncProxyServer.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport android.net.Uri;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.http.AsyncHttpClient;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.http.AsyncHttpResponse;\nimport com.jeffmony.async.http.callback.HttpConnectCallback;\n\n/**\n * Created by koush on 7/22/14.\n */\npublic class AsyncProxyServer extends AsyncHttpServer {\n    AsyncHttpClient proxyClient;\n    public AsyncProxyServer(AsyncServer server) {\n        proxyClient = new AsyncHttpClient(server);\n    }\n\n    @Override\n    protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {\n        super.onRequest(callback, request, response);\n\n        if (callback != null)\n            return;\n\n        try {\n            Uri uri;\n\n            try {\n                uri = Uri.parse(request.getPath());\n                if (uri.getScheme() == null)\n                    throw new Exception(\"no host or full uri provided\");\n            }\n            catch (Exception e) {\n                String host = request.getHeaders().get(\"Host\");\n                int port = 80;\n                if (host != null) {\n                    String[] splits = host.split(\":\", 2);\n                    if (splits.length == 2) {\n                        host = splits[0];\n                        port = Integer.parseInt(splits[1]);\n                    }\n                }\n                uri = Uri.parse(\"http://\" + host + \":\" + port + request.getPath());\n            }\n\n            proxyClient.execute(new AsyncHttpRequest(uri, request.getMethod(), request.getHeaders()), new HttpConnectCallback() {\n                @Override\n                public void onConnectCompleted(Exception ex, AsyncHttpResponse remoteResponse) {\n                    if (ex != null) {\n                        response.code(500);\n                        response.send(ex.getMessage());\n                        return;\n                    }\n                    response.proxy(remoteResponse);\n                }\n            });\n        }\n        catch (Exception e) {\n            response.code(500);\n            response.send(e.getMessage());\n        }\n    }\n\n    @Override\n    protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/BoundaryEmitter.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.FilteredDataEmitter;\n\nimport java.nio.ByteBuffer;\n\npublic class BoundaryEmitter extends FilteredDataEmitter {\n    private byte[] boundary;\n    public void setBoundary(String boundary) {\n        this.boundary = (\"\\r\\n--\" + boundary).getBytes();\n    }\n    \n    public String getBoundary() {\n        if (boundary == null)\n            return null;\n        return new String(boundary, 4, boundary.length - 4);\n    }\n    \n    public String getBoundaryStart() {\n        assert boundary != null;\n        return new String(boundary, 2, boundary.length - 2);\n    }\n    \n    public String getBoundaryEnd() {\n        assert boundary != null;\n        return getBoundaryStart() + \"--\\r\\n\";\n    }\n    \n    protected void onBoundaryStart() {\n    }\n    \n    protected void onBoundaryEnd() {\n    }\n    \n    // >= 0 matching\n    // -1 matching - (start of boundary end) or \\r (boundary start)\n    // -2 matching - (end of boundary end)\n    // -3 matching \\r after boundary\n    // -4 matching \\n after boundary\n\n    // the state starts out having already matched \\r\\n\n\n    /*\n    Mac:work$ curl -F person=anonymous -F secret=@test.kt  http://localhost:5555\n\n    POST / HTTP/1.1\n    Host: localhost:5555\n    User-Agent: curl/7.54.0\n    Content-Length: 372\n    Expect: 100-continue\n    Content-Type: multipart/form-data; boundary=------------------------17903558439eb6ff\n\n--------------------------17903558439eb6ff              <--- note! two dashes before boundary\n    Content-Disposition: form-data; name=\"person\"\n\n    anonymous\n--------------------------17903558439eb6ff              <--- note! two dashes before boundary\n    Content-Disposition: form-data; name=\"secret\"; filename=\"test.kt\"\n    Content-Type: application/octet-stream\n\n    fun main(args: Array<String>) {\n        println(\"Hello JavaScript!\")\n    }\n\n--------------------------17903558439eb6ff--            <--- note! two dashes before AND after boundary\n            */\n    \n    \n    int state = 2;\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n//        System.out.println(bb.getString());\n//        System.out.println(\"chunk: \" + bb.remaining());\n        \n//        System.out.println(\"state: \" + state);\n        \n        // if we were in the middle of a potential match, let's throw that\n        // at the beginning of the buffer and process it too.\n        if (state > 0) {\n            ByteBuffer b = ByteBufferList.obtain(boundary.length);\n            b.put(boundary, 0, state);\n            b.flip();\n            bb.addFirst(b);\n            state = 0;\n        }\n        \n        int last = 0;\n        byte[] buf = new byte[bb.remaining()];\n        bb.get(buf);\n        for (int i = 0; i < buf.length; i++) {\n            if (state >= 0) {\n                if (buf[i] == boundary[state]) {\n                    state++;\n                    if (state == boundary.length)\n                        state = -1;\n                }\n                else if (state > 0) {\n                    // let's try matching again one byte after the start\n                    // of last match occurrence\n                    i -= state;\n                    state = 0;\n                }\n            }\n            else if (state == -1) {\n                if (buf[i] == '\\r') {\n                    state = -4;\n                    int len = i - last - boundary.length;\n                    if (last != 0 || len != 0) {\n                        ByteBuffer b = ByteBufferList.obtain(len).put(buf, last, len);\n                        b.flip();\n                        ByteBufferList list = new ByteBufferList();\n                        list.add(b);\n                        super.onDataAvailable(this, list);\n                    }\n//                    System.out.println(\"bstart\");\n                    onBoundaryStart();\n                }\n                else if (buf[i] == '-') {\n                    state = -2;\n                }\n                else {\n                    report(new MimeEncodingException(\"Invalid multipart/form-data. Expected \\r or -\"));\n                    return;\n                }\n            }\n            else if (state == -2) {\n                if (buf[i] == '-') {\n                    state = -3;\n                }\n                else {\n                    report(new MimeEncodingException(\"Invalid multipart/form-data. Expected -\"));\n                    return;\n                }\n            }\n            else if (state == -3) {\n                if (buf[i] == '\\r') {\n                    state = -4;\n                    ByteBuffer b = ByteBufferList.obtain(i - last - boundary.length - 2).put(buf, last, i - last - boundary.length - 2);\n                    b.flip();\n                    ByteBufferList list = new ByteBufferList();\n                    list.add(b);\n                    super.onDataAvailable(this, list);\n//                    System.out.println(\"bend\");\n                    onBoundaryEnd();\n                }\n                else {\n                    report(new MimeEncodingException(\"Invalid multipart/form-data. Expected \\r\"));\n                    return;\n                }\n            }\n            else if (state == -4) {\n                if (buf[i] == '\\n') {\n                    last = i + 1;\n                    state = 0;\n                }\n                else {\n                    report(new MimeEncodingException(\"Invalid multipart/form-data. Expected \\n\"));\n                }\n            }\n            else {\n                assert false;\n                report(new MimeEncodingException(\"Invalid multipart/form-data. Unknown state?\"));\n            }\n        }\n\n        if (last < buf.length) {\n//            System.out.println(\"amount left at boundary: \" + (buf.length - last));\n//            System.out.println(\"State: \" + state);\n//            System.out.println(state);\n            int keep = Math.max(state, 0);\n            ByteBuffer b = ByteBufferList.obtain(buf.length - last - keep).put(buf, last, buf.length - last - keep);\n            b.flip();\n            ByteBufferList list = new ByteBufferList();\n            list.add(b);\n            super.onDataAvailable(this, list);\n        }\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/HttpServerRequestCallback.java",
    "content": "package com.jeffmony.async.http.server;\n\n\npublic interface HttpServerRequestCallback {\n    void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response);\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/MalformedRangeException.java",
    "content": "package com.jeffmony.async.http.server;\n\npublic class MalformedRangeException extends Exception {\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/MimeEncodingException.java",
    "content": "package com.jeffmony.async.http.server;\n\npublic class MimeEncodingException extends Exception {\n    public MimeEncodingException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/RouteMatcher.java",
    "content": "package com.jeffmony.async.http.server;\n\npublic interface RouteMatcher {\n    AsyncHttpServerRouter.RouteMatch route(String method, String path);\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/StreamSkipException.java",
    "content": "package com.jeffmony.async.http.server;\n\npublic class StreamSkipException extends Exception {\n    public StreamSkipException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/http/server/UnknownRequestBody.java",
    "content": "package com.jeffmony.async.http.server;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.http.AsyncHttpRequest;\nimport com.jeffmony.async.http.body.AsyncHttpRequestBody;\n\npublic class UnknownRequestBody implements AsyncHttpRequestBody<Void> {\n    public UnknownRequestBody(String contentType) {\n        mContentType = contentType;\n    }\n\n    int length = -1;\n    public UnknownRequestBody(DataEmitter emitter, String contentType, int length) {\n        mContentType = contentType;\n        this.emitter = emitter;\n        this.length = length;\n    }\n\n    @Override\n    public void write(final AsyncHttpRequest request, DataSink sink, final CompletedCallback completed) {\n        Util.pump(emitter, sink, completed);\n        if (emitter.isPaused())\n            emitter.resume();\n    }\n\n    private String mContentType;\n    @Override\n    public String getContentType() {\n        return mContentType;\n    }\n\n    @Override\n    public boolean readFullyOnRequest() {\n        return false;\n    }\n\n    @Override\n    public int length() {\n        return length;\n    }\n\n    @Override\n    public Void get() {\n        return null;\n    }\n\n    @Deprecated\n    public void setCallbacks(DataCallback callback, CompletedCallback endCallback) {\n        emitter.setEndCallback(endCallback);\n        emitter.setDataCallback(callback);\n    }\n\n    public DataEmitter getEmitter() {\n        return emitter;\n    }\n\n    DataEmitter emitter;\n    @Override\n    public void parse(DataEmitter emitter, CompletedCallback completed) {\n        this.emitter = emitter;\n        emitter.setEndCallback(completed);\n        emitter.setDataCallback(new DataCallback.NullDataCallback());\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/AsyncParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\n\nimport java.lang.reflect.Type;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic interface AsyncParser<T> {\n    Future<T> parse(DataEmitter emitter);\n    void write(DataSink sink, T value, CompletedCallback completed);\n    Type getType();\n    String getMime();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/ByteBufferListParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.future.SimpleFuture;\n\nimport java.lang.reflect.Type;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class ByteBufferListParser implements AsyncParser<ByteBufferList> {\n    @Override\n    public Future<ByteBufferList> parse(final DataEmitter emitter) {\n        final ByteBufferList bb = new ByteBufferList();\n        final SimpleFuture<ByteBufferList> ret = new SimpleFuture<ByteBufferList>() {\n            @Override\n            protected void cancelCleanup() {\n                emitter.close();\n            }\n        };\n        emitter.setDataCallback(new DataCallback() {\n            @Override\n            public void onDataAvailable(DataEmitter emitter, ByteBufferList data) {\n                data.get(bb);\n            }\n        });\n\n        emitter.setEndCallback(new CompletedCallback() {\n            @Override\n            public void onCompleted(Exception ex) {\n                if (ex != null) {\n                    ret.setComplete(ex);\n                    return;\n                }\n\n                try {\n                    ret.setComplete(bb);\n                }\n                catch (Exception e) {\n                    ret.setComplete(e);\n                }\n            }\n        });\n\n        return ret;\n    }\n\n    @Override\n    public void write(DataSink sink, ByteBufferList value, CompletedCallback completed) {\n        Util.writeAll(sink, value, completed);\n    }\n\n    @Override\n    public Type getType() {\n        return ByteBufferList.class;\n    }\n\n    @Override\n    public String getMime() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/DocumentParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\nimport com.jeffmony.async.http.body.DocumentBody;\nimport com.jeffmony.async.stream.ByteBufferListInputStream;\n\nimport org.w3c.dom.Document;\n\nimport java.lang.reflect.Type;\n\nimport javax.xml.parsers.DocumentBuilderFactory;\n\n/**\n * Created by koush on 8/3/13.\n */\npublic class DocumentParser implements AsyncParser<Document> {\n    @Override\n    public Future<Document> parse(DataEmitter emitter) {\n        return new ByteBufferListParser().parse(emitter)\n        .thenConvert(from -> DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteBufferListInputStream(from)));\n    }\n\n    @Override\n    public void write(DataSink sink, Document value, CompletedCallback completed) {\n        new DocumentBody(value).write(null, sink, completed);\n    }\n\n    @Override\n    public Type getType() {\n        return Document.class;\n    }\n\n    @Override\n    public String getMime() {\n        return \"text/xml\";\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/JSONArrayParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\n\nimport org.json.JSONArray;\n\nimport java.lang.reflect.Type;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class JSONArrayParser implements AsyncParser<JSONArray> {\n    @Override\n    public Future<JSONArray> parse(DataEmitter emitter) {\n        return new StringParser().parse(emitter)\n        .thenConvert(JSONArray::new);\n    }\n\n    @Override\n    public void write(DataSink sink, JSONArray value, CompletedCallback completed) {\n        new StringParser().write(sink, value.toString(), completed);\n    }\n\n    @Override\n    public Type getType() {\n        return JSONArray.class;\n    }\n\n    @Override\n    public String getMime() {\n        return \"application/json\";\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/JSONObjectParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\n\nimport org.json.JSONObject;\n\nimport java.lang.reflect.Type;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class JSONObjectParser implements AsyncParser<JSONObject> {\n    @Override\n    public Future<JSONObject> parse(DataEmitter emitter) {\n        return new StringParser().parse(emitter).thenConvert(JSONObject::new);\n    }\n\n    @Override\n    public void write(DataSink sink, JSONObject value, CompletedCallback completed) {\n        new StringParser().write(sink, value.toString(), completed);\n    }\n\n    @Override\n    public Type getType() {\n        return JSONObject.class;\n    }\n\n    @Override\n    public String getMime() {\n        return \"application/json\";\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/parser/StringParser.java",
    "content": "package com.jeffmony.async.parser;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.future.Future;\n\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class StringParser implements AsyncParser<String> {\n    Charset forcedCharset;\n\n    public StringParser() {\n    }\n\n    public StringParser(Charset charset) {\n        this.forcedCharset = charset;\n    }\n\n    @Override\n    public Future<String> parse(DataEmitter emitter) {\n        final String charset = emitter.charset();\n        return new ByteBufferListParser().parse(emitter)\n        .thenConvert(from -> {\n            Charset charsetToUse = forcedCharset;\n            if (charsetToUse == null && charset != null)\n                charsetToUse = Charset.forName(charset);\n            return from.readString(charsetToUse);\n        });\n    }\n\n    @Override\n    public void write(DataSink sink, String value, CompletedCallback completed) {\n        new ByteBufferListParser().write(sink, new ByteBufferList(value.getBytes()), completed);\n    }\n\n    @Override\n    public Type getType() {\n        return String.class;\n    }\n\n    @Override\n    public String getMime() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/stream/ByteBufferListInputStream.java",
    "content": "package com.jeffmony.async.stream;\n\nimport com.jeffmony.async.ByteBufferList;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Created by koush on 6/1/13.\n */\npublic class ByteBufferListInputStream extends InputStream {\n    ByteBufferList bb;\n    public ByteBufferListInputStream(ByteBufferList bb) {\n        this.bb = bb;\n    }\n\n    @Override\n    public int read() throws IOException {\n        if (bb.remaining() <= 0)\n            return -1;\n        return (int)bb.get() & 0x000000ff;\n    }\n\n    @Override\n    public int read(byte[] buffer) throws IOException {\n        return this.read(buffer, 0, buffer.length);\n    }\n\n    @Override\n    public int read(byte[] buffer, int offset, int length) throws IOException {\n        if (bb.remaining() <= 0)\n            return -1;\n        int toRead = Math.min(length, bb.remaining());\n        bb.get(buffer, offset, toRead);\n        return toRead;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/stream/FileDataSink.java",
    "content": "package com.jeffmony.async.stream;\n\nimport com.jeffmony.async.AsyncServer;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * Created by koush on 2/2/14.\n */\npublic class FileDataSink extends OutputStreamDataSink {\n    File file;\n    public FileDataSink(AsyncServer server, File file) {\n        super(server);\n        this.file = file;\n    }\n\n    @Override\n    public OutputStream getOutputStream() throws IOException {\n        OutputStream ret = super.getOutputStream();\n        if (ret == null) {\n            file.getParentFile().mkdirs();\n            ret = new FileOutputStream(file);\n            setOutputStream(ret);\n        }\n        return ret;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/stream/InputStreamDataEmitter.java",
    "content": "package com.jeffmony.async.stream;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.Util;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\n\n/**\n * Created by koush on 5/22/13.\n */\npublic class InputStreamDataEmitter implements DataEmitter {\n    AsyncServer server;\n    InputStream inputStream;\n    public InputStreamDataEmitter(AsyncServer server, InputStream inputStream) {\n        this.server = server;\n        this.inputStream = inputStream;\n        doResume();\n    }\n\n    DataCallback callback;\n    @Override\n    public void setDataCallback(DataCallback callback) {\n        this.callback = callback;\n    }\n\n    @Override\n    public DataCallback getDataCallback() {\n        return callback;\n    }\n\n    @Override\n    public boolean isChunked() {\n        return false;\n    }\n\n    boolean paused;\n    @Override\n    public void pause() {\n        paused = true;\n    }\n\n    @Override\n    public void resume() {\n        paused = false;\n        doResume();\n    }\n\n    private void report(final Exception e) {\n        getServer().post(new Runnable() {\n            @Override\n            public void run() {\n                Exception ex = e;\n                try {\n                    inputStream.close();\n                }\n                catch (Exception e) {\n                    ex = e;\n                }\n                if (endCallback != null)\n                    endCallback.onCompleted(ex);\n            }\n        });\n    }\n\n    int mToAlloc = 0;\n    ByteBufferList pending = new ByteBufferList();\n    Runnable pumper = new Runnable() {\n        @Override\n        public void run() {\n            try {\n                if (!pending.isEmpty()) {\n                    getServer().run(new Runnable() {\n                        @Override\n                        public void run() {\n                            Util.emitAllData(InputStreamDataEmitter.this, pending);\n                        }\n                    });\n                    if (!pending.isEmpty())\n                        return;\n                }\n                ByteBuffer b;\n                do {\n                    b = ByteBufferList.obtain(Math.min(Math.max(mToAlloc, 2 << 11), 256 * 1024));\n                    int read;\n                    if (-1 == (read = inputStream.read(b.array()))) {\n                        report(null);\n                        return;\n                    }\n                    mToAlloc = read * 2;\n                    b.limit(read);\n                    pending.add(b);\n                    getServer().run(new Runnable() {\n                        @Override\n                        public void run() {\n                            Util.emitAllData(InputStreamDataEmitter.this, pending);\n                        }\n                    });\n                }\n                while (pending.remaining() == 0 && !isPaused());\n            }\n            catch (Exception e) {\n                report(e);\n            }\n        }\n    };\n\n    private void doResume() {\n        new Thread(pumper).start();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return paused;\n    }\n\n    CompletedCallback endCallback;\n    @Override\n    public void setEndCallback(CompletedCallback callback) {\n        endCallback = callback;\n    }\n\n    @Override\n    public CompletedCallback getEndCallback() {\n        return endCallback;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return server;\n    }\n\n    @Override\n    public void close() {\n        report(null);\n        try {\n            inputStream.close();\n        }\n        catch (Exception e) {\n        }\n    }\n\n    @Override\n    public String charset() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/stream/OutputStreamDataCallback.java",
    "content": "package com.jeffmony.async.stream;\n\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataEmitter;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.DataCallback;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\n\npublic class OutputStreamDataCallback implements DataCallback, CompletedCallback {\n    private OutputStream mOutput;\n    public OutputStreamDataCallback(OutputStream os) {\n        mOutput = os;\n    }\n\n    public OutputStream getOutputStream() {\n        return mOutput;\n    }\n\n    @Override\n    public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {\n        try {\n            while (bb.size() > 0) {\n                ByteBuffer b = bb.remove();\n                mOutput.write(b.array(), b.arrayOffset() + b.position(), b.remaining());\n                ByteBufferList.reclaim(b);\n            }\n        }\n        catch (Exception ex) {\n            onCompleted(ex);\n        }\n        finally {\n            bb.recycle();\n        }\n    }\n    \n    public void close() {\n        try {\n            mOutput.close();\n        }\n        catch (IOException e) {\n            onCompleted(e);\n        }\n    }\n\n    @Override\n    public void onCompleted(Exception error) {\n        error.printStackTrace();       \n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/stream/OutputStreamDataSink.java",
    "content": "package com.jeffmony.async.stream;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.ByteBufferList;\nimport com.jeffmony.async.DataSink;\nimport com.jeffmony.async.callback.CompletedCallback;\nimport com.jeffmony.async.callback.WritableCallback;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\n\npublic class OutputStreamDataSink implements DataSink {\n    public OutputStreamDataSink(AsyncServer server) {\n        this(server, null);\n    }\n\n    @Override\n    public void end() {\n        try {\n            if (mStream != null)\n                mStream.close();\n            reportClose(null);\n        }\n        catch (IOException e) {\n            reportClose(e);\n        }\n    }\n\n    AsyncServer server;\n    public OutputStreamDataSink(AsyncServer server, OutputStream stream) {\n        this.server = server;\n        setOutputStream(stream);\n    }\n\n    OutputStream mStream;\n    public void setOutputStream(OutputStream stream) {\n        mStream = stream;\n    }\n    \n    public OutputStream getOutputStream() throws IOException {\n        return mStream;\n    }\n\n    @Override\n    public void write(final ByteBufferList bb) {\n        try {\n            while (bb.size() > 0) {\n                ByteBuffer b = bb.remove();\n                getOutputStream().write(b.array(), b.arrayOffset() + b.position(), b.remaining());\n                ByteBufferList.reclaim(b);\n            }\n        }\n        catch (IOException e) {\n            reportClose(e);\n        }\n        finally {\n            bb.recycle();\n        }\n    }\n\n    WritableCallback mWritable;\n    @Override\n    public void setWriteableCallback(WritableCallback handler) {\n        mWritable = handler;        \n    }\n\n    @Override\n    public WritableCallback getWriteableCallback() {\n        return mWritable;\n    }\n\n    @Override\n    public boolean isOpen() {\n        return closeReported;\n    }\n\n    boolean closeReported;\n    Exception closeException;\n    public void reportClose(Exception ex) {\n        if (closeReported)\n            return;\n        closeReported = true;\n        closeException = ex;\n\n        if (mClosedCallback != null)\n            mClosedCallback.onCompleted(closeException);\n    }\n    \n    CompletedCallback mClosedCallback;\n    @Override\n    public void setClosedCallback(CompletedCallback handler) {\n        mClosedCallback = handler;        \n    }\n\n    @Override\n    public CompletedCallback getClosedCallback() {\n        return mClosedCallback;\n    }\n\n    @Override\n    public AsyncServer getServer() {\n        return server;\n    }\n\n    WritableCallback outputStreamCallback;\n    public void setOutputStreamWritableCallback(WritableCallback outputStreamCallback) {\n        this.outputStreamCallback = outputStreamCallback;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/Allocator.java",
    "content": "package com.jeffmony.async.util;\n\nimport com.jeffmony.async.ByteBufferList;\n\nimport java.nio.ByteBuffer;\n\n/**\n * Created by koush on 6/28/14.\n */\npublic class Allocator {\n    final int maxAlloc;\n    int currentAlloc = 0;\n    int minAlloc = 2 << 11;\n\n    public Allocator(int maxAlloc) {\n        this.maxAlloc = maxAlloc;\n    }\n\n    public Allocator() {\n        maxAlloc = ByteBufferList.MAX_ITEM_SIZE;\n    }\n\n    public ByteBuffer allocate() {\n        return allocate(currentAlloc);\n    }\n\n    public ByteBuffer allocate(int currentAlloc) {\n        return ByteBufferList.obtain(Math.min(Math.max(currentAlloc, minAlloc), maxAlloc));\n    }\n\n    public void track(long read) {\n        currentAlloc = (int)read * 2;\n    }\n\n    public int getMaxAlloc() {\n        return maxAlloc;\n    }\n\n    public void setCurrentAlloc(int currentAlloc) {\n        this.currentAlloc = currentAlloc;\n    }\n\n    public int getMinAlloc() {\n        return minAlloc;\n    }\n\n    public Allocator setMinAlloc(int minAlloc ) {\n        this.minAlloc = Math.max(0, minAlloc);\n        return this;\n    }\n}\n\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/ArrayDeque.java",
    "content": "/*\n * Written by Josh Bloch of Google Inc. and released to the public domain,\n * as explained at http://creativecommons.org/publicdomain/zero/1.0/.\n */\n\npackage com.jeffmony.async.util;\n\n// BEGIN android-note\n// removed link to collections framework docs\n// END android-note\n\nimport java.util.AbstractCollection;\nimport java.util.Collection;\nimport java.util.ConcurrentModificationException;\nimport java.util.Iterator;\nimport java.util.NoSuchElementException;\n\n/**\n * Resizable-array implementation of the {@link Deque} interface.  Array\n * deques have no capacity restrictions; they grow as necessary to support\n * usage.  They are not thread-safe; in the absence of external\n * synchronization, they do not support concurrent access by multiple threads.\n * Null elements are prohibited.  This class is likely to be faster than\n * {@link java.util.Stack} when used as a stack, and faster than {@link java.util.LinkedList}\n * when used as a queue.\n *\n * <p>Most <tt>ArrayDeque</tt> operations run in amortized constant time.\n * Exceptions include {@link #remove(Object) remove}, {@link\n * #removeFirstOccurrence removeFirstOccurrence}, {@link #removeLastOccurrence\n * removeLastOccurrence}, {@link #contains contains}, {@link #iterator\n * iterator.remove()}, and the bulk operations, all of which run in linear\n * time.\n *\n * <p>The iterators returned by this class's <tt>iterator</tt> method are\n * <i>fail-fast</i>: If the deque is modified at any time after the iterator\n * is created, in any way except through the iterator's own <tt>remove</tt>\n * method, the iterator will generally throw a {@link\n * ConcurrentModificationException}.  Thus, in the face of concurrent\n * modification, the iterator fails quickly and cleanly, rather than risking\n * arbitrary, non-deterministic behavior at an undetermined time in the\n * future.\n *\n * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed\n * as it is, generally speaking, impossible to make any hard guarantees in the\n * presence of unsynchronized concurrent modification.  Fail-fast iterators\n * throw <tt>ConcurrentModificationException</tt> on a best-effort basis.\n * Therefore, it would be wrong to write a program that depended on this\n * exception for its correctness: <i>the fail-fast behavior of iterators\n * should be used only to detect bugs.</i>\n *\n * <p>This class and its iterator implement all of the\n * <em>optional</em> methods of the {@link Collection} and {@link\n * Iterator} interfaces.\n *\n * @author  Josh Bloch and Doug Lea\n * @since   1.6\n * @param <E> the type of elements held in this collection\n */\npublic class ArrayDeque<E> extends AbstractCollection<E>\n                           implements Deque<E>, Cloneable, java.io.Serializable\n{\n    /**\n     * The array in which the elements of the deque are stored.\n     * The capacity of the deque is the length of this array, which is\n     * always a power of two. The array is never allowed to become\n     * full, except transiently within an addX method where it is\n     * resized (see doubleCapacity) immediately upon becoming full,\n     * thus avoiding head and tail wrapping around to equal each\n     * other.  We also guarantee that all array cells not holding\n     * deque elements are always null.\n     */\n    private transient Object[] elements;\n\n    /**\n     * The index of the element at the head of the deque (which is the\n     * element that would be removed by remove() or pop()); or an\n     * arbitrary number equal to tail if the deque is empty.\n     */\n    private transient int head;\n\n    /**\n     * The index at which the next element would be added to the tail\n     * of the deque (via addLast(E), add(E), or push(E)).\n     */\n    private transient int tail;\n\n    /**\n     * The minimum capacity that we'll use for a newly created deque.\n     * Must be a power of 2.\n     */\n    private static final int MIN_INITIAL_CAPACITY = 8;\n\n    // ******  Array allocation and resizing utilities ******\n\n    /**\n     * Allocate empty array to hold the given number of elements.\n     *\n     * @param numElements  the number of elements to hold\n     */\n    private void allocateElements(int numElements) {\n        int initialCapacity = MIN_INITIAL_CAPACITY;\n        // Find the best power of two to hold elements.\n        // Tests \"<=\" because arrays aren't kept full.\n        if (numElements >= initialCapacity) {\n            initialCapacity = numElements;\n            initialCapacity |= (initialCapacity >>>  1);\n            initialCapacity |= (initialCapacity >>>  2);\n            initialCapacity |= (initialCapacity >>>  4);\n            initialCapacity |= (initialCapacity >>>  8);\n            initialCapacity |= (initialCapacity >>> 16);\n            initialCapacity++;\n\n            if (initialCapacity < 0)   // Too many elements, must back off\n                initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements\n        }\n        elements = new Object[initialCapacity];\n    }\n\n    /**\n     * Double the capacity of this deque.  Call only when full, i.e.,\n     * when head and tail have wrapped around to become equal.\n     */\n    private void doubleCapacity() {\n        assert head == tail;\n        int p = head;\n        int n = elements.length;\n        int r = n - p; // number of elements to the right of p\n        int newCapacity = n << 1;\n        if (newCapacity < 0)\n            throw new IllegalStateException(\"Sorry, deque too big\");\n        Object[] a = new Object[newCapacity];\n        System.arraycopy(elements, p, a, 0, r);\n        System.arraycopy(elements, 0, a, r, p);\n        elements = a;\n        head = 0;\n        tail = n;\n    }\n\n    /**\n     * Copies the elements from our element array into the specified array,\n     * in order (from first to last element in the deque).  It is assumed\n     * that the array is large enough to hold all elements in the deque.\n     *\n     * @return its argument\n     */\n    private <T> T[] copyElements(T[] a) {\n        if (head < tail) {\n            System.arraycopy(elements, head, a, 0, size());\n        } else if (head > tail) {\n            int headPortionLen = elements.length - head;\n            System.arraycopy(elements, head, a, 0, headPortionLen);\n            System.arraycopy(elements, 0, a, headPortionLen, tail);\n        }\n        return a;\n    }\n\n    /**\n     * Constructs an empty array deque with an initial capacity\n     * sufficient to hold 16 elements.\n     */\n    public ArrayDeque() {\n        elements = new Object[16];\n    }\n\n    /**\n     * Constructs an empty array deque with an initial capacity\n     * sufficient to hold the specified number of elements.\n     *\n     * @param numElements  lower bound on initial capacity of the deque\n     */\n    public ArrayDeque(int numElements) {\n        allocateElements(numElements);\n    }\n\n    /**\n     * Constructs a deque containing the elements of the specified\n     * collection, in the order they are returned by the collection's\n     * iterator.  (The first element returned by the collection's\n     * iterator becomes the first element, or <i>front</i> of the\n     * deque.)\n     *\n     * @param c the collection whose elements are to be placed into the deque\n     * @throws NullPointerException if the specified collection is null\n     */\n    public ArrayDeque(Collection<? extends E> c) {\n        allocateElements(c.size());\n        addAll(c);\n    }\n\n    // The main insertion and extraction methods are addFirst,\n    // addLast, pollFirst, pollLast. The other methods are defined in\n    // terms of these.\n\n    /**\n     * Inserts the specified element at the front of this deque.\n     *\n     * @param e the element to add\n     * @throws NullPointerException if the specified element is null\n     */\n    public void addFirst(E e) {\n        if (e == null)\n            throw new NullPointerException(\"e == null\");\n        elements[head = (head - 1) & (elements.length - 1)] = e;\n        if (head == tail)\n            doubleCapacity();\n    }\n\n    /**\n     * Inserts the specified element at the end of this deque.\n     *\n     * <p>This method is equivalent to {@link #add}.\n     *\n     * @param e the element to add\n     * @throws NullPointerException if the specified element is null\n     */\n    public void addLast(E e) {\n        if (e == null)\n            throw new NullPointerException(\"e == null\");\n        elements[tail] = e;\n        if ( (tail = (tail + 1) & (elements.length - 1)) == head)\n            doubleCapacity();\n    }\n\n    /**\n     * Inserts the specified element at the front of this deque.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> (as specified by {@link Deque#offerFirst})\n     * @throws NullPointerException if the specified element is null\n     */\n    public boolean offerFirst(E e) {\n        addFirst(e);\n        return true;\n    }\n\n    /**\n     * Inserts the specified element at the end of this deque.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> (as specified by {@link Deque#offerLast})\n     * @throws NullPointerException if the specified element is null\n     */\n    public boolean offerLast(E e) {\n        addLast(e);\n        return true;\n    }\n\n    /**\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E removeFirst() {\n        E x = pollFirst();\n        if (x == null)\n            throw new NoSuchElementException();\n        return x;\n    }\n\n    /**\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E removeLast() {\n        E x = pollLast();\n        if (x == null)\n            throw new NoSuchElementException();\n        return x;\n    }\n\n    public E pollFirst() {\n        int h = head;\n        @SuppressWarnings(\"unchecked\") E result = (E) elements[h];\n        // Element is null if deque empty\n        if (result == null)\n            return null;\n        elements[h] = null;     // Must null out slot\n        head = (h + 1) & (elements.length - 1);\n        return result;\n    }\n\n    public E pollLast() {\n        int t = (tail - 1) & (elements.length - 1);\n        @SuppressWarnings(\"unchecked\") E result = (E) elements[t];\n        if (result == null)\n            return null;\n        elements[t] = null;\n        tail = t;\n        return result;\n    }\n\n    /**\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E getFirst() {\n        @SuppressWarnings(\"unchecked\") E result = (E) elements[head];\n        if (result == null)\n            throw new NoSuchElementException();\n        return result;\n    }\n\n    /**\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E getLast() {\n        @SuppressWarnings(\"unchecked\")\n        E result = (E) elements[(tail - 1) & (elements.length - 1)];\n        if (result == null)\n            throw new NoSuchElementException();\n        return result;\n    }\n\n    public E peekFirst() {\n        @SuppressWarnings(\"unchecked\") E result = (E) elements[head];\n        // elements[head] is null if deque empty\n        return result;\n    }\n\n    public E peekLast() {\n        @SuppressWarnings(\"unchecked\")\n        E result = (E) elements[(tail - 1) & (elements.length - 1)];\n        return result;\n    }\n\n    /**\n     * Removes the first occurrence of the specified element in this\n     * deque (when traversing the deque from head to tail).\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the first element <tt>e</tt> such that\n     * <tt>o.equals(e)</tt> (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if the deque contained the specified element\n     */\n    public boolean removeFirstOccurrence(Object o) {\n        if (o == null)\n            return false;\n        int mask = elements.length - 1;\n        int i = head;\n        Object x;\n        while ( (x = elements[i]) != null) {\n            if (o.equals(x)) {\n                delete(i);\n                return true;\n            }\n            i = (i + 1) & mask;\n        }\n        return false;\n    }\n\n    /**\n     * Removes the last occurrence of the specified element in this\n     * deque (when traversing the deque from head to tail).\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the last element <tt>e</tt> such that\n     * <tt>o.equals(e)</tt> (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if the deque contained the specified element\n     */\n    public boolean removeLastOccurrence(Object o) {\n        if (o == null)\n            return false;\n        int mask = elements.length - 1;\n        int i = (tail - 1) & mask;\n        Object x;\n        while ( (x = elements[i]) != null) {\n            if (o.equals(x)) {\n                delete(i);\n                return true;\n            }\n            i = (i - 1) & mask;\n        }\n        return false;\n    }\n\n    // *** Queue methods ***\n\n    /**\n     * Inserts the specified element at the end of this deque.\n     *\n     * <p>This method is equivalent to {@link #addLast}.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> (as specified by {@link Collection#add})\n     * @throws NullPointerException if the specified element is null\n     */\n    public boolean add(E e) {\n        addLast(e);\n        return true;\n    }\n\n    /**\n     * Inserts the specified element at the end of this deque.\n     *\n     * <p>This method is equivalent to {@link #offerLast}.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> (as specified by {@link java.util.Queue#offer})\n     * @throws NullPointerException if the specified element is null\n     */\n    public boolean offer(E e) {\n        return offerLast(e);\n    }\n\n    /**\n     * Retrieves and removes the head of the queue represented by this deque.\n     *\n     * This method differs from {@link #poll poll} only in that it throws an\n     * exception if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #removeFirst}.\n     *\n     * @return the head of the queue represented by this deque\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E remove() {\n        return removeFirst();\n    }\n\n    /**\n     * Retrieves and removes the head of the queue represented by this deque\n     * (in other words, the first element of this deque), or returns\n     * <tt>null</tt> if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #pollFirst}.\n     *\n     * @return the head of the queue represented by this deque, or\n     *         <tt>null</tt> if this deque is empty\n     */\n    public E poll() {\n        return pollFirst();\n    }\n\n    /**\n     * Retrieves, but does not remove, the head of the queue represented by\n     * this deque.  This method differs from {@link #peek peek} only in\n     * that it throws an exception if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #getFirst}.\n     *\n     * @return the head of the queue represented by this deque\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E element() {\n        return getFirst();\n    }\n\n    /**\n     * Retrieves, but does not remove, the head of the queue represented by\n     * this deque, or returns <tt>null</tt> if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #peekFirst}.\n     *\n     * @return the head of the queue represented by this deque, or\n     *         <tt>null</tt> if this deque is empty\n     */\n    public E peek() {\n        return peekFirst();\n    }\n\n    // *** Stack methods ***\n\n    /**\n     * Pushes an element onto the stack represented by this deque.  In other\n     * words, inserts the element at the front of this deque.\n     *\n     * <p>This method is equivalent to {@link #addFirst}.\n     *\n     * @param e the element to push\n     * @throws NullPointerException if the specified element is null\n     */\n    public void push(E e) {\n        addFirst(e);\n    }\n\n    /**\n     * Pops an element from the stack represented by this deque.  In other\n     * words, removes and returns the first element of this deque.\n     *\n     * <p>This method is equivalent to {@link #removeFirst()}.\n     *\n     * @return the element at the front of this deque (which is the top\n     *         of the stack represented by this deque)\n     * @throws NoSuchElementException {@inheritDoc}\n     */\n    public E pop() {\n        return removeFirst();\n    }\n\n    private void checkInvariants() {\n        assert elements[tail] == null;\n        assert head == tail ? elements[head] == null :\n            (elements[head] != null &&\n             elements[(tail - 1) & (elements.length - 1)] != null);\n        assert elements[(head - 1) & (elements.length - 1)] == null;\n    }\n\n    /**\n     * Removes the element at the specified position in the elements array,\n     * adjusting head and tail as necessary.  This can result in motion of\n     * elements backwards or forwards in the array.\n     *\n     * <p>This method is called delete rather than remove to emphasize\n     * that its semantics differ from those of {@link java.util.List#remove(int)}.\n     *\n     * @return true if elements moved backwards\n     */\n    private boolean delete(int i) {\n        checkInvariants();\n        final Object[] elements = this.elements;\n        final int mask = elements.length - 1;\n        final int h = head;\n        final int t = tail;\n        final int front = (i - h) & mask;\n        final int back  = (t - i) & mask;\n\n        // Invariant: head <= i < tail mod circularity\n        if (front >= ((t - h) & mask))\n            throw new ConcurrentModificationException();\n\n        // Optimize for least element motion\n        if (front < back) {\n            if (h <= i) {\n                System.arraycopy(elements, h, elements, h + 1, front);\n            } else { // Wrap around\n                System.arraycopy(elements, 0, elements, 1, i);\n                elements[0] = elements[mask];\n                System.arraycopy(elements, h, elements, h + 1, mask - h);\n            }\n            elements[h] = null;\n            head = (h + 1) & mask;\n            return false;\n        } else {\n            if (i < t) { // Copy the null tail as well\n                System.arraycopy(elements, i + 1, elements, i, back);\n                tail = t - 1;\n            } else { // Wrap around\n                System.arraycopy(elements, i + 1, elements, i, mask - i);\n                elements[mask] = elements[0];\n                System.arraycopy(elements, 1, elements, 0, t);\n                tail = (t - 1) & mask;\n            }\n            return true;\n        }\n    }\n\n    // *** Collection Methods ***\n\n    /**\n     * Returns the number of elements in this deque.\n     *\n     * @return the number of elements in this deque\n     */\n    public int size() {\n        return (tail - head) & (elements.length - 1);\n    }\n\n    /**\n     * Returns <tt>true</tt> if this deque contains no elements.\n     *\n     * @return <tt>true</tt> if this deque contains no elements\n     */\n    public boolean isEmpty() {\n        return head == tail;\n    }\n\n    /**\n     * Returns an iterator over the elements in this deque.  The elements\n     * will be ordered from first (head) to last (tail).  This is the same\n     * order that elements would be dequeued (via successive calls to\n     * {@link #remove} or popped (via successive calls to {@link #pop}).\n     *\n     * @return an iterator over the elements in this deque\n     */\n    public Iterator<E> iterator() {\n        return new DeqIterator();\n    }\n\n    public Iterator<E> descendingIterator() {\n        return new DescendingIterator();\n    }\n\n    private class DeqIterator implements Iterator<E> {\n        /**\n         * Index of element to be returned by subsequent call to next.\n         */\n        private int cursor = head;\n\n        /**\n         * Tail recorded at construction (also in remove), to stop\n         * iterator and also to check for comodification.\n         */\n        private int fence = tail;\n\n        /**\n         * Index of element returned by most recent call to next.\n         * Reset to -1 if element is deleted by a call to remove.\n         */\n        private int lastRet = -1;\n\n        public boolean hasNext() {\n            return cursor != fence;\n        }\n\n        public E next() {\n            if (cursor == fence)\n                throw new NoSuchElementException();\n            @SuppressWarnings(\"unchecked\") E result = (E) elements[cursor];\n            // This check doesn't catch all possible comodifications,\n            // but does catch the ones that corrupt traversal\n            if (tail != fence || result == null)\n                throw new ConcurrentModificationException();\n            lastRet = cursor;\n            cursor = (cursor + 1) & (elements.length - 1);\n            return result;\n        }\n\n        public void remove() {\n            if (lastRet < 0)\n                throw new IllegalStateException();\n            if (delete(lastRet)) { // if left-shifted, undo increment in next()\n                cursor = (cursor - 1) & (elements.length - 1);\n                fence = tail;\n            }\n            lastRet = -1;\n        }\n    }\n\n    private class DescendingIterator implements Iterator<E> {\n        /*\n         * This class is nearly a mirror-image of DeqIterator, using\n         * tail instead of head for initial cursor, and head instead of\n         * tail for fence.\n         */\n        private int cursor = tail;\n        private int fence = head;\n        private int lastRet = -1;\n\n        public boolean hasNext() {\n            return cursor != fence;\n        }\n\n        public E next() {\n            if (cursor == fence)\n                throw new NoSuchElementException();\n            cursor = (cursor - 1) & (elements.length - 1);\n            @SuppressWarnings(\"unchecked\") E result = (E) elements[cursor];\n            if (head != fence || result == null)\n                throw new ConcurrentModificationException();\n            lastRet = cursor;\n            return result;\n        }\n\n        public void remove() {\n            if (lastRet < 0)\n                throw new IllegalStateException();\n            if (!delete(lastRet)) {\n                cursor = (cursor + 1) & (elements.length - 1);\n                fence = head;\n            }\n            lastRet = -1;\n        }\n    }\n\n    /**\n     * Returns <tt>true</tt> if this deque contains the specified element.\n     * More formally, returns <tt>true</tt> if and only if this deque contains\n     * at least one element <tt>e</tt> such that <tt>o.equals(e)</tt>.\n     *\n     * @param o object to be checked for containment in this deque\n     * @return <tt>true</tt> if this deque contains the specified element\n     */\n    public boolean contains(Object o) {\n        if (o == null)\n            return false;\n        int mask = elements.length - 1;\n        int i = head;\n        Object x;\n        while ( (x = elements[i]) != null) {\n            if (o.equals(x))\n                return true;\n            i = (i + 1) & mask;\n        }\n        return false;\n    }\n\n    /**\n     * Removes a single instance of the specified element from this deque.\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the first element <tt>e</tt> such that\n     * <tt>o.equals(e)</tt> (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * <p>This method is equivalent to {@link #removeFirstOccurrence}.\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if this deque contained the specified element\n     */\n    public boolean remove(Object o) {\n        return removeFirstOccurrence(o);\n    }\n\n    /**\n     * Removes all of the elements from this deque.\n     * The deque will be empty after this call returns.\n     */\n    public void clear() {\n        int h = head;\n        int t = tail;\n        if (h != t) { // clear all cells\n            head = tail = 0;\n            int i = h;\n            int mask = elements.length - 1;\n            do {\n                elements[i] = null;\n                i = (i + 1) & mask;\n            } while (i != t);\n        }\n    }\n\n    /**\n     * Returns an array containing all of the elements in this deque\n     * in proper sequence (from first to last element).\n     *\n     * <p>The returned array will be \"safe\" in that no references to it are\n     * maintained by this deque.  (In other words, this method must allocate\n     * a new array).  The caller is thus free to modify the returned array.\n     *\n     * <p>This method acts as bridge between array-based and collection-based\n     * APIs.\n     *\n     * @return an array containing all of the elements in this deque\n     */\n    public Object[] toArray() {\n        return copyElements(new Object[size()]);\n    }\n\n    /**\n     * Returns an array containing all of the elements in this deque in\n     * proper sequence (from first to last element); the runtime type of the\n     * returned array is that of the specified array.  If the deque fits in\n     * the specified array, it is returned therein.  Otherwise, a new array\n     * is allocated with the runtime type of the specified array and the\n     * size of this deque.\n     *\n     * <p>If this deque fits in the specified array with room to spare\n     * (i.e., the array has more elements than this deque), the element in\n     * the array immediately following the end of the deque is set to\n     * <tt>null</tt>.\n     *\n     * <p>Like the {@link #toArray()} method, this method acts as bridge between\n     * array-based and collection-based APIs.  Further, this method allows\n     * precise control over the runtime type of the output array, and may,\n     * under certain circumstances, be used to save allocation costs.\n     *\n     * <p>Suppose <tt>x</tt> is a deque known to contain only strings.\n     * The following code can be used to dump the deque into a newly\n     * allocated array of <tt>String</tt>:\n     *\n     *  <pre> {@code String[] y = x.toArray(new String[0]);}</pre>\n     *\n     * Note that <tt>toArray(new Object[0])</tt> is identical in function to\n     * <tt>toArray()</tt>.\n     *\n     * @param a the array into which the elements of the deque are to\n     *          be stored, if it is big enough; otherwise, a new array of the\n     *          same runtime type is allocated for this purpose\n     * @return an array containing all of the elements in this deque\n     * @throws ArrayStoreException if the runtime type of the specified array\n     *         is not a supertype of the runtime type of every element in\n     *         this deque\n     * @throws NullPointerException if the specified array is null\n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T> T[] toArray(T[] a) {\n        int size = size();\n        if (a.length < size)\n            a = (T[])java.lang.reflect.Array.newInstance(\n                    a.getClass().getComponentType(), size);\n        copyElements(a);\n        if (a.length > size)\n            a[size] = null;\n        return a;\n    }\n\n    // *** Object methods ***\n\n    /**\n     * Returns a copy of this deque.\n     *\n     * @return a copy of this deque\n     */\n    public ArrayDeque<E> clone() {\n        try {\n            @SuppressWarnings(\"unchecked\")\n            ArrayDeque<E> result = (ArrayDeque<E>) super.clone();\n            System.arraycopy(elements, 0, result.elements, 0, elements.length);\n            return result;\n\n        } catch (CloneNotSupportedException e) {\n            throw new AssertionError();\n        }\n    }\n\n    /**\n     * Appease the serialization gods.\n     */\n    private static final long serialVersionUID = 2340985798034038923L;\n\n    /**\n     * Serialize this deque.\n     *\n     * @serialData The current size (<tt>int</tt>) of the deque,\n     * followed by all of its elements (each an object reference) in\n     * first-to-last order.\n     */\n    private void writeObject(java.io.ObjectOutputStream s)\n            throws java.io.IOException {\n        s.defaultWriteObject();\n\n        // Write out size\n        s.writeInt(size());\n\n        // Write out elements in order.\n        int mask = elements.length - 1;\n        for (int i = head; i != tail; i = (i + 1) & mask)\n            s.writeObject(elements[i]);\n    }\n\n    /**\n     * Deserialize this deque.\n     */\n    private void readObject(java.io.ObjectInputStream s)\n            throws java.io.IOException, ClassNotFoundException {\n        s.defaultReadObject();\n\n        // Read in size and allocate array\n        int size = s.readInt();\n        allocateElements(size);\n        head = 0;\n        tail = size;\n\n        // Read in all elements in the proper order.\n        for (int i = 0; i < size; i++)\n            elements[i] = s.readObject();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/Charsets.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.nio.charset.Charset;\n\n/** From java.nio.charset.Charsets */\npublic class Charsets {\n  public static final Charset US_ASCII = Charset.forName(\"US-ASCII\");\n  public static final Charset UTF_8 = Charset.forName(\"UTF-8\");\n  public static final Charset ISO_8859_1 = Charset.forName(\"ISO-8859-1\");\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/Deque.java",
    "content": "/*\n * Written by Doug Lea and Josh Bloch with assistance from members of\n * JCP JSR-166 Expert Group and released to the public domain, as explained\n * at http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.jeffmony.async.util;\n\n// BEGIN android-note\n// removed link to collections framework docs\n// END android-note\n\nimport java.util.Iterator;\nimport java.util.Queue;\n\n/**\n * A linear collection that supports element insertion and removal at\n * both ends.  The name <i>deque</i> is short for \"double ended queue\"\n * and is usually pronounced \"deck\".  Most <tt>Deque</tt>\n * implementations place no fixed limits on the number of elements\n * they may contain, but this interface supports capacity-restricted\n * deques as well as those with no fixed size limit.\n *\n * <p>This interface defines methods to access the elements at both\n * ends of the deque.  Methods are provided to insert, remove, and\n * examine the element.  Each of these methods exists in two forms:\n * one throws an exception if the operation fails, the other returns a\n * special value (either <tt>null</tt> or <tt>false</tt>, depending on\n * the operation).  The latter form of the insert operation is\n * designed specifically for use with capacity-restricted\n * <tt>Deque</tt> implementations; in most implementations, insert\n * operations cannot fail.\n *\n * <p>The twelve methods described above are summarized in the\n * following table:\n *\n * <p>\n * <table BORDER CELLPADDING=3 CELLSPACING=1>\n *  <tr>\n *    <td></td>\n *    <td ALIGN=CENTER COLSPAN = 2> <b>First Element (Head)</b></td>\n *    <td ALIGN=CENTER COLSPAN = 2> <b>Last Element (Tail)</b></td>\n *  </tr>\n *  <tr>\n *    <td></td>\n *    <td ALIGN=CENTER><em>Throws exception</em></td>\n *    <td ALIGN=CENTER><em>Special value</em></td>\n *    <td ALIGN=CENTER><em>Throws exception</em></td>\n *    <td ALIGN=CENTER><em>Special value</em></td>\n *  </tr>\n *  <tr>\n *    <td><b>Insert</b></td>\n *    <td>{@link #addFirst addFirst(e)}</td>\n *    <td>{@link #offerFirst offerFirst(e)}</td>\n *    <td>{@link #addLast addLast(e)}</td>\n *    <td>{@link #offerLast offerLast(e)}</td>\n *  </tr>\n *  <tr>\n *    <td><b>Remove</b></td>\n *    <td>{@link #removeFirst removeFirst()}</td>\n *    <td>{@link #pollFirst pollFirst()}</td>\n *    <td>{@link #removeLast removeLast()}</td>\n *    <td>{@link #pollLast pollLast()}</td>\n *  </tr>\n *  <tr>\n *    <td><b>Examine</b></td>\n *    <td>{@link #getFirst getFirst()}</td>\n *    <td>{@link #peekFirst peekFirst()}</td>\n *    <td>{@link #getLast getLast()}</td>\n *    <td>{@link #peekLast peekLast()}</td>\n *  </tr>\n * </table>\n *\n * <p>This interface extends the {@link Queue} interface.  When a deque is\n * used as a queue, FIFO (First-In-First-Out) behavior results.  Elements are\n * added at the end of the deque and removed from the beginning.  The methods\n * inherited from the <tt>Queue</tt> interface are precisely equivalent to\n * <tt>Deque</tt> methods as indicated in the following table:\n *\n * <p>\n * <table BORDER CELLPADDING=3 CELLSPACING=1>\n *  <tr>\n *    <td ALIGN=CENTER> <b><tt>Queue</tt> Method</b></td>\n *    <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#add add(e)}</td>\n *    <td>{@link #addLast addLast(e)}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#offer offer(e)}</td>\n *    <td>{@link #offerLast offerLast(e)}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#remove remove()}</td>\n *    <td>{@link #removeFirst removeFirst()}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#poll poll()}</td>\n *    <td>{@link #pollFirst pollFirst()}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#element element()}</td>\n *    <td>{@link #getFirst getFirst()}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link java.util.Queue#peek peek()}</td>\n *    <td>{@link #peek peekFirst()}</td>\n *  </tr>\n * </table>\n *\n * <p>Deques can also be used as LIFO (Last-In-First-Out) stacks.  This\n * interface should be used in preference to the legacy {@link java.util.Stack} class.\n * When a deque is used as a stack, elements are pushed and popped from the\n * beginning of the deque.  Stack methods are precisely equivalent to\n * <tt>Deque</tt> methods as indicated in the table below:\n *\n * <p>\n * <table BORDER CELLPADDING=3 CELLSPACING=1>\n *  <tr>\n *    <td ALIGN=CENTER> <b>Stack Method</b></td>\n *    <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>\n *  </tr>\n *  <tr>\n *    <td>{@link #push push(e)}</td>\n *    <td>{@link #addFirst addFirst(e)}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link #pop pop()}</td>\n *    <td>{@link #removeFirst removeFirst()}</td>\n *  </tr>\n *  <tr>\n *    <td>{@link #peek peek()}</td>\n *    <td>{@link #peekFirst peekFirst()}</td>\n *  </tr>\n * </table>\n *\n * <p>Note that the {@link #peek peek} method works equally well when\n * a deque is used as a queue or a stack; in either case, elements are\n * drawn from the beginning of the deque.\n *\n * <p>This interface provides two methods to remove interior\n * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and\n * {@link #removeLastOccurrence removeLastOccurrence}.\n *\n * <p>Unlike the {@link java.util.List} interface, this interface does not\n * provide support for indexed access to elements.\n *\n * <p>While <tt>Deque</tt> implementations are not strictly required\n * to prohibit the insertion of null elements, they are strongly\n * encouraged to do so.  Users of any <tt>Deque</tt> implementations\n * that do allow null elements are strongly encouraged <i>not</i> to\n * take advantage of the ability to insert nulls.  This is so because\n * <tt>null</tt> is used as a special return value by various methods\n * to indicated that the deque is empty.\n *\n * <p><tt>Deque</tt> implementations generally do not define\n * element-based versions of the <tt>equals</tt> and <tt>hashCode</tt>\n * methods, but instead inherit the identity-based versions from class\n * <tt>Object</tt>.\n *\n * @author Doug Lea\n * @author Josh Bloch\n * @since  1.6\n * @param <E> the type of elements held in this collection\n */\n\npublic interface Deque<E> extends Queue<E> {\n    /**\n     * Inserts the specified element at the front of this deque if it is\n     * possible to do so immediately without violating capacity restrictions.\n     * When using a capacity-restricted deque, it is generally preferable to\n     * use method {@link #offerFirst}.\n     *\n     * @param e the element to add\n     * @throws IllegalStateException if the element cannot be added at this\n     *         time due to capacity restrictions\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    void addFirst(E e);\n\n    /**\n     * Inserts the specified element at the end of this deque if it is\n     * possible to do so immediately without violating capacity restrictions.\n     * When using a capacity-restricted deque, it is generally preferable to\n     * use method {@link #offerLast}.\n     *\n     * <p>This method is equivalent to {@link #add}.\n     *\n     * @param e the element to add\n     * @throws IllegalStateException if the element cannot be added at this\n     *         time due to capacity restrictions\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    void addLast(E e);\n\n    /**\n     * Inserts the specified element at the front of this deque unless it would\n     * violate capacity restrictions.  When using a capacity-restricted deque,\n     * this method is generally preferable to the {@link #addFirst} method,\n     * which can fail to insert an element only by throwing an exception.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> if the element was added to this deque, else\n     *         <tt>false</tt>\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    boolean offerFirst(E e);\n\n    /**\n     * Inserts the specified element at the end of this deque unless it would\n     * violate capacity restrictions.  When using a capacity-restricted deque,\n     * this method is generally preferable to the {@link #addLast} method,\n     * which can fail to insert an element only by throwing an exception.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> if the element was added to this deque, else\n     *         <tt>false</tt>\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    boolean offerLast(E e);\n\n    /**\n     * Retrieves and removes the first element of this deque.  This method\n     * differs from {@link #pollFirst pollFirst} only in that it throws an\n     * exception if this deque is empty.\n     *\n     * @return the head of this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E removeFirst();\n\n    /**\n     * Retrieves and removes the last element of this deque.  This method\n     * differs from {@link #pollLast pollLast} only in that it throws an\n     * exception if this deque is empty.\n     *\n     * @return the tail of this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E removeLast();\n\n    /**\n     * Retrieves and removes the first element of this deque,\n     * or returns <tt>null</tt> if this deque is empty.\n     *\n     * @return the head of this deque, or <tt>null</tt> if this deque is empty\n     */\n    E pollFirst();\n\n    /**\n     * Retrieves and removes the last element of this deque,\n     * or returns <tt>null</tt> if this deque is empty.\n     *\n     * @return the tail of this deque, or <tt>null</tt> if this deque is empty\n     */\n    E pollLast();\n\n    /**\n     * Retrieves, but does not remove, the first element of this deque.\n     *\n     * This method differs from {@link #peekFirst peekFirst} only in that it\n     * throws an exception if this deque is empty.\n     *\n     * @return the head of this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E getFirst();\n\n    /**\n     * Retrieves, but does not remove, the last element of this deque.\n     * This method differs from {@link #peekLast peekLast} only in that it\n     * throws an exception if this deque is empty.\n     *\n     * @return the tail of this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E getLast();\n\n    /**\n     * Retrieves, but does not remove, the first element of this deque,\n     * or returns <tt>null</tt> if this deque is empty.\n     *\n     * @return the head of this deque, or <tt>null</tt> if this deque is empty\n     */\n    E peekFirst();\n\n    /**\n     * Retrieves, but does not remove, the last element of this deque,\n     * or returns <tt>null</tt> if this deque is empty.\n     *\n     * @return the tail of this deque, or <tt>null</tt> if this deque is empty\n     */\n    E peekLast();\n\n    /**\n     * Removes the first occurrence of the specified element from this deque.\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the first element <tt>e</tt> such that\n     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>\n     * (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if an element was removed as a result of this call\n     * @throws ClassCastException if the class of the specified element\n     *         is incompatible with this deque (optional)\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements (optional)\n     */\n    boolean removeFirstOccurrence(Object o);\n\n    /**\n     * Removes the last occurrence of the specified element from this deque.\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the last element <tt>e</tt> such that\n     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>\n     * (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if an element was removed as a result of this call\n     * @throws ClassCastException if the class of the specified element\n     *         is incompatible with this deque (optional)\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements (optional)\n     */\n    boolean removeLastOccurrence(Object o);\n\n    // *** Queue methods ***\n\n    /**\n     * Inserts the specified element into the queue represented by this deque\n     * (in other words, at the tail of this deque) if it is possible to do so\n     * immediately without violating capacity restrictions, returning\n     * <tt>true</tt> upon success and throwing an\n     * <tt>IllegalStateException</tt> if no space is currently available.\n     * When using a capacity-restricted deque, it is generally preferable to\n     * use {@link #offer(Object) offer}.\n     *\n     * <p>This method is equivalent to {@link #addLast}.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> (as specified by {@link java.util.Collection#add})\n     * @throws IllegalStateException if the element cannot be added at this\n     *         time due to capacity restrictions\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    boolean add(E e);\n\n    /**\n     * Inserts the specified element into the queue represented by this deque\n     * (in other words, at the tail of this deque) if it is possible to do so\n     * immediately without violating capacity restrictions, returning\n     * <tt>true</tt> upon success and <tt>false</tt> if no space is currently\n     * available.  When using a capacity-restricted deque, this method is\n     * generally preferable to the {@link #add} method, which can fail to\n     * insert an element only by throwing an exception.\n     *\n     * <p>This method is equivalent to {@link #offerLast}.\n     *\n     * @param e the element to add\n     * @return <tt>true</tt> if the element was added to this deque, else\n     *         <tt>false</tt>\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    boolean offer(E e);\n\n    /**\n     * Retrieves and removes the head of the queue represented by this deque\n     * (in other words, the first element of this deque).\n     * This method differs from {@link #poll poll} only in that it throws an\n     * exception if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #removeFirst()}.\n     *\n     * @return the head of the queue represented by this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E remove();\n\n    /**\n     * Retrieves and removes the head of the queue represented by this deque\n     * (in other words, the first element of this deque), or returns\n     * <tt>null</tt> if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #pollFirst()}.\n     *\n     * @return the first element of this deque, or <tt>null</tt> if\n     *         this deque is empty\n     */\n    E poll();\n\n    /**\n     * Retrieves, but does not remove, the head of the queue represented by\n     * this deque (in other words, the first element of this deque).\n     * This method differs from {@link #peek peek} only in that it throws an\n     * exception if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #getFirst()}.\n     *\n     * @return the head of the queue represented by this deque\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E element();\n\n    /**\n     * Retrieves, but does not remove, the head of the queue represented by\n     * this deque (in other words, the first element of this deque), or\n     * returns <tt>null</tt> if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #peekFirst()}.\n     *\n     * @return the head of the queue represented by this deque, or\n     *         <tt>null</tt> if this deque is empty\n     */\n    E peek();\n\n\n    // *** Stack methods ***\n\n    /**\n     * Pushes an element onto the stack represented by this deque (in other\n     * words, at the head of this deque) if it is possible to do so\n     * immediately without violating capacity restrictions, returning\n     * <tt>true</tt> upon success and throwing an\n     * <tt>IllegalStateException</tt> if no space is currently available.\n     *\n     * <p>This method is equivalent to {@link #addFirst}.\n     *\n     * @param e the element to push\n     * @throws IllegalStateException if the element cannot be added at this\n     *         time due to capacity restrictions\n     * @throws ClassCastException if the class of the specified element\n     *         prevents it from being added to this deque\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements\n     * @throws IllegalArgumentException if some property of the specified\n     *         element prevents it from being added to this deque\n     */\n    void push(E e);\n\n    /**\n     * Pops an element from the stack represented by this deque.  In other\n     * words, removes and returns the first element of this deque.\n     *\n     * <p>This method is equivalent to {@link #removeFirst()}.\n     *\n     * @return the element at the front of this deque (which is the top\n     *         of the stack represented by this deque)\n     * @throws java.util.NoSuchElementException if this deque is empty\n     */\n    E pop();\n\n\n    // *** Collection methods ***\n\n    /**\n     * Removes the first occurrence of the specified element from this deque.\n     * If the deque does not contain the element, it is unchanged.\n     * More formally, removes the first element <tt>e</tt> such that\n     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>\n     * (if such an element exists).\n     * Returns <tt>true</tt> if this deque contained the specified element\n     * (or equivalently, if this deque changed as a result of the call).\n     *\n     * <p>This method is equivalent to {@link #removeFirstOccurrence}.\n     *\n     * @param o element to be removed from this deque, if present\n     * @return <tt>true</tt> if an element was removed as a result of this call\n     * @throws ClassCastException if the class of the specified element\n     *         is incompatible with this deque (optional)\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements (optional)\n     */\n    boolean remove(Object o);\n\n    /**\n     * Returns <tt>true</tt> if this deque contains the specified element.\n     * More formally, returns <tt>true</tt> if and only if this deque contains\n     * at least one element <tt>e</tt> such that\n     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.\n     *\n     * @param o element whose presence in this deque is to be tested\n     * @return <tt>true</tt> if this deque contains the specified element\n     * @throws ClassCastException if the type of the specified element\n     *         is incompatible with this deque (optional)\n     * @throws NullPointerException if the specified element is null and this\n     *         deque does not permit null elements (optional)\n     */\n    boolean contains(Object o);\n\n    /**\n     * Returns the number of elements in this deque.\n     *\n     * @return the number of elements in this deque\n     */\n    public int size();\n\n    /**\n     * Returns an iterator over the elements in this deque in proper sequence.\n     * The elements will be returned in order from first (head) to last (tail).\n     *\n     * @return an iterator over the elements in this deque in proper sequence\n     */\n    Iterator<E> iterator();\n\n    /**\n     * Returns an iterator over the elements in this deque in reverse\n     * sequential order.  The elements will be returned in order from\n     * last (tail) to first (head).\n     *\n     * @return an iterator over the elements in this deque in reverse\n     * sequence\n     */\n    Iterator<E> descendingIterator();\n\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/FileCache.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.Provider;\nimport java.security.Security;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.Random;\nimport java.util.Set;\n\n/**\n * Created by koush on 4/12/14.\n */\npublic class FileCache {\n    class CacheEntry {\n        final long size;\n        public CacheEntry(File file) {\n            size = file.length();\n        }\n    }\n\n    public static class Snapshot {\n        FileInputStream[] fins;\n        long[] lens;\n        Snapshot(FileInputStream[] fins, long[] lens) {\n            this.fins = fins;\n            this.lens = lens;\n        }\n\n        public long getLength(int index) {\n            return lens[index];\n        }\n\n        public void close() {\n            StreamUtility.closeQuietly(fins);\n        }\n    }\n\n    private static String hashAlgorithm = \"MD5\";\n\n    private static MessageDigest findAlternativeMessageDigest() {\n        if (\"MD5\".equals(hashAlgorithm)) {\n            for (Provider provider : Security.getProviders()) {\n                for (Provider.Service service : provider.getServices()) {\n                    hashAlgorithm = service.getAlgorithm();\n                    try {\n                        MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm);\n                        if (messageDigest != null)\n                            return messageDigest;\n                    } catch (NoSuchAlgorithmException ignored) {\n                    }\n                }\n            }\n        }\n        return null;\n    }\n\n    static MessageDigest messageDigest;\n    static {\n        try {\n            messageDigest = MessageDigest.getInstance(hashAlgorithm);\n        } catch (NoSuchAlgorithmException e) {\n            messageDigest = findAlternativeMessageDigest();\n            if (null == messageDigest)\n                throw new RuntimeException(e);\n        }\n        try {\n            messageDigest = (MessageDigest)messageDigest.clone();\n        }\n        catch (CloneNotSupportedException e) {\n        }\n    }\n\n    public static synchronized String toKeyString(Object... parts) {\n        messageDigest.reset();\n        for (Object part : parts) {\n            messageDigest.update(part.toString().getBytes());\n        }\n        byte[] md5bytes = messageDigest.digest();\n        return new BigInteger(1, md5bytes).toString(16);\n    }\n\n    boolean loadAsync;\n    Random random = new Random();\n    public File getTempFile() {\n        File f;\n        while ((f = new File(directory, new BigInteger(128, random).toString(16))).exists());\n        return f;\n    }\n\n    public File[] getTempFiles(int count) {\n        File[] ret = new File[count];\n        for (int i = 0; i < count; i++) {\n            ret[i] = getTempFile();\n        }\n        return ret;\n    }\n\n    public static void removeFiles(File... files) {\n        if (files == null)\n            return;\n        for (File file: files) {\n            file.delete();\n        }\n    }\n\n    public void remove(String key) {\n        int i = 0;\n        while (cache.remove(getPartName(key, i)) != null) {\n            i++;\n        }\n        removePartFiles(key);\n    }\n\n    public boolean exists(String key, int part) {\n        return getPartFile(key, part).exists();\n    }\n\n    public boolean exists(String key) {\n        return getPartFile(key, 0).exists();\n    }\n\n    public File touch(File file) {\n        cache.get(file.getName());\n        file.setLastModified(System.currentTimeMillis());\n        return file;\n    }\n\n    public FileInputStream get(String key) throws IOException {\n        return new FileInputStream(touch(getPartFile(key, 0)));\n    }\n\n    public File getFile(String key) {\n        return touch(getPartFile(key, 0));\n    }\n\n    public FileInputStream[] get(String key, int count) throws IOException {\n        FileInputStream[] ret = new FileInputStream[count];\n        try {\n            for (int i = 0; i < count; i++) {\n                ret[i] = new FileInputStream(touch(getPartFile(key, i)));\n            }\n        }\n        catch (IOException e) {\n            // if we can't get all the parts, delete everything\n            for (FileInputStream fin: ret) {\n                StreamUtility.closeQuietly(fin);\n            }\n            remove(key);\n            throw e;\n        }\n\n        return ret;\n    }\n\n    String getPartName(String key, int part) {\n        return key + \".\" + part;\n    }\n\n    public void commitTempFiles(String key, File... tempFiles) {\n        removePartFiles(key);\n\n        // try to rename everything\n        for (int i = 0; i < tempFiles.length; i++) {\n            File tmp = tempFiles[i];\n            File partFile = getPartFile(key, i);\n            if (!tmp.renameTo(partFile)) {\n                // if any rename fails, delete everything\n                removeFiles(tempFiles);\n                remove(key);\n                return;\n            }\n            remove(tmp.getName());\n            cache.put(getPartName(key, i), new CacheEntry(partFile));\n        }\n    }\n\n    void removePartFiles(String key) {\n        int i = 0;\n        File f;\n        while ((f = getPartFile(key, i)).exists()) {\n            f.delete();\n            i++;\n        }\n    }\n\n    File getPartFile(String key, int part) {\n        return new File(directory, getPartName(key, part));\n    }\n\n    long blockSize = 4096;\n    public void setBlockSize(long blockSize) {\n        this.blockSize = blockSize;\n    }\n\n    class InternalCache extends LruCache<String, CacheEntry> {\n        public InternalCache() {\n            super(size);\n        }\n\n        @Override\n        protected long sizeOf(String key, CacheEntry value) {\n            return Math.max(blockSize, value.size);\n        }\n\n        @Override\n        protected void entryRemoved(boolean evicted, String key, CacheEntry oldValue, CacheEntry newValue) {\n            super.entryRemoved(evicted, key, oldValue, newValue);\n            if (newValue != null)\n                return;\n            if (loading)\n                return;\n            new File(directory, key).delete();\n        }\n    }\n\n    InternalCache cache;\n    File directory;\n    long size;\n\n    Comparator<File> dateCompare = new Comparator<File>() {\n        @Override\n        public int compare(File lhs, File rhs) {\n            long l = lhs.lastModified();\n            long r = rhs.lastModified();\n            if (l < r)\n                return -1;\n            if (r > l)\n                return 1;\n            return 0;\n        }\n    };\n\n    boolean loading;\n    void load() {\n        loading = true;\n        try {\n            File[] files = directory.listFiles();\n            if (files == null)\n                return;\n            ArrayList<File> list = new ArrayList<File>();\n            Collections.addAll(list, files);\n            Collections.sort(list, dateCompare);\n\n            for (File file: list) {\n                String name = file.getName();\n                CacheEntry entry = new CacheEntry(file);\n                cache.put(name, entry);\n                cache.get(name);\n            }\n        }\n        finally {\n            loading = false;\n        }\n    }\n\n    private void doLoad() {\n        if (loadAsync) {\n            new Thread() {\n                @Override\n                public void run() {\n                    load();\n                }\n            }.start();\n        }\n        else {\n            load();\n        }\n    }\n\n    public FileCache(File directory, long size, boolean loadAsync) {\n        this.directory = directory;\n        this.size = size;\n        this.loadAsync = loadAsync;\n        cache = new InternalCache();\n\n        directory.mkdirs();\n        doLoad();\n    }\n\n    public long size() {\n        return cache.size();\n    }\n\n    public void clear() {\n        removeFiles(directory.listFiles());\n        cache.evictAll();\n    }\n\n    public Set<String> keySet() {\n        HashSet<String> ret = new HashSet<String>();\n        File[] files = directory.listFiles();\n        if (files == null)\n            return ret;\n        for (File file: files) {\n            String name = file.getName();\n            int last = name.lastIndexOf('.');\n            if (last != -1)\n                ret.add(name.substring(0, last));\n        }\n        return ret;\n    }\n\n    public void setMaxSize(long maxSize) {\n        cache.setMaxSize(maxSize);\n        doLoad();\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/FileUtility.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.io.File;\n\n/**\n * Created by koush on 4/7/14.\n */\npublic class FileUtility {\n    static public boolean deleteDirectory(File path) {\n        if (path.exists()) {\n            File[] files = path.listFiles();\n            if (files != null) {\n                for (int i = 0; i < files.length; i++) {\n                    if (files[i].isDirectory()) {\n                        deleteDirectory(files[i]);\n                    }\n                    else {\n                        files[i].delete();\n                    }\n                }\n            }\n        }\n        return (path.delete());\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/HashList.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.util.ArrayList;\nimport java.util.Hashtable;\nimport java.util.Set;\n\n/**\n * Created by koush on 5/27/13.\n */\npublic class HashList<T> {\n    Hashtable<String, TaggedList<T>> internal = new Hashtable<String, TaggedList<T>>();\n\n    public HashList() {\n    }\n\n    public Set<String> keySet() {\n        return internal.keySet();\n    }\n\n    public synchronized <V> V tag(String key) {\n        TaggedList<T> list = internal.get(key);\n        if (list == null)\n            return null;\n        return list.tag();\n    }\n\n    public synchronized <V> void tag(String key, V tag) {\n        TaggedList<T> list = internal.get(key);\n        if (list == null) {\n            list = new TaggedList<T>();\n            internal.put(key, list);\n        }\n        list.tag(tag);\n    }\n\n    public synchronized ArrayList<T> remove(String key) {\n        return internal.remove(key);\n    }\n\n    public synchronized int size() {\n        return internal.size();\n    }\n\n    public synchronized ArrayList<T> get(String key) {\n        return internal.get(key);\n    }\n\n    synchronized public boolean contains(String key) {\n        ArrayList<T> check = get(key);\n        return check != null && check.size() > 0;\n    }\n\n    synchronized public void add(String key, T value) {\n        ArrayList<T> ret = get(key);\n        if (ret == null) {\n            TaggedList<T> put = new TaggedList<T>();\n            ret = put;\n            internal.put(key, put);\n        }\n        ret.add(value);\n    }\n\n    synchronized public T pop(String key) {\n        TaggedList<T> values = internal.get(key);\n        if (values == null)\n            return null;\n        if (values.size() == 0)\n            return null;\n        return values.remove(values.size() - 1);\n    }\n\n    synchronized public boolean removeItem(String key, T value) {\n        TaggedList<T> values = internal.get(key);\n        if (values == null)\n            return false;\n\n        values.remove(value);\n        return values.size() == 0;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/IdleTimeout.java",
    "content": "package com.jeffmony.async.util;\n\nimport android.os.Handler;\n\nimport com.jeffmony.async.AsyncServer;\n\npublic class IdleTimeout extends TimeoutBase {\n    Runnable callback;\n\n    public IdleTimeout(AsyncServer server, long delay) {\n        super(server, delay);\n\n    }\n\n    public IdleTimeout(Handler handler, long delay) {\n        super(handler, delay);\n    }\n\n    public void setTimeout(Runnable callback) {\n        this.callback = callback;\n    }\n\n    Object cancellable;\n    public void reset() {\n        handlerish.removeAllCallbacks(cancellable);\n        cancellable = handlerish.postDelayed(callback, delay);\n    }\n\n    public void cancel() {\n        // must post this, so that when it runs it removes everything in the queue,\n        // preventing any rescheduling.\n        // posting gaurantees there is not a reschedule in progress.\n        handlerish.post(() -> handlerish.removeAllCallbacks(cancellable));\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/LruCache.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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\npackage com.jeffmony.async.util;\n\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\n\n/**\n * Static library version of {@link android.util.LruCache}. Used to write apps\n * that run on API levels prior to 12. When running on API level 12 or above,\n * this implementation is still used; it does not try to switch to the\n * framework's implementation. See the framework SDK documentation for a class\n * overview.\n */\npublic class LruCache<K, V> {\n    private final LinkedHashMap<K, V> map;\n\n    /** Size of this cache in units. Not necessarily the number of elements. */\n    private long size;\n    private long maxSize;\n\n    private int putCount;\n    private int createCount;\n    private int evictionCount;\n    private int hitCount;\n    private int missCount;\n\n    /**\n     * @param maxSize for caches that do not override {@link #sizeOf}, this is\n     *     the maximum number of entries in the cache. For all other caches,\n     *     this is the maximum sum of the sizes of the entries in this cache.\n     */\n    public LruCache(long maxSize) {\n        if (maxSize <= 0) {\n            throw new IllegalArgumentException(\"maxSize <= 0\");\n        }\n        this.maxSize = maxSize;\n        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);\n    }\n\n    /**\n     * Returns the value for {@code key} if it exists in the cache or can be\n     * created by {@code #create}. If a value was returned, it is moved to the\n     * head of the queue. This returns null if a value is not cached and cannot\n     * be created.\n     */\n    public final V get(K key) {\n        if (key == null) {\n            throw new NullPointerException(\"key == null\");\n        }\n\n        V mapValue;\n        synchronized (this) {\n            mapValue = map.get(key);\n            if (mapValue != null) {\n                hitCount++;\n                return mapValue;\n            }\n            missCount++;\n        }\n\n        /*\n         * Attempt to create a value. This may take a long time, and the map\n         * may be different when create() returns. If a conflicting value was\n         * added to the map while create() was working, we leave that value in\n         * the map and release the created value.\n         */\n\n        V createdValue = create(key);\n        if (createdValue == null) {\n            return null;\n        }\n\n        synchronized (this) {\n            createCount++;\n            mapValue = map.put(key, createdValue);\n\n            if (mapValue != null) {\n                // There was a conflict so undo that last put\n                map.put(key, mapValue);\n            } else {\n                size += safeSizeOf(key, createdValue);\n            }\n        }\n\n        if (mapValue != null) {\n            entryRemoved(false, key, createdValue, mapValue);\n            return mapValue;\n        } else {\n            trimToSize(maxSize);\n            return createdValue;\n        }\n    }\n\n    /**\n     * Caches {@code value} for {@code key}. The value is moved to the head of\n     * the queue.\n     *\n     * @return the previous value mapped by {@code key}.\n     */\n    public final V put(K key, V value) {\n        if (key == null || value == null) {\n            throw new NullPointerException(\"key == null || value == null\");\n        }\n\n        V previous;\n        synchronized (this) {\n            putCount++;\n            size += safeSizeOf(key, value);\n            previous = map.put(key, value);\n            if (previous != null) {\n                size -= safeSizeOf(key, previous);\n            }\n        }\n\n        if (previous != null) {\n            entryRemoved(false, key, previous, value);\n        }\n\n        trimToSize(maxSize);\n        return previous;\n    }\n\n    /**\n     * @param maxSize the maximum size of the cache before returning. May be -1\n     *     to evict even 0-sized elements.\n     */\n    private void trimToSize(long maxSize) {\n        while (true) {\n            K key;\n            V value;\n            synchronized (this) {\n                if (size < 0 || (map.isEmpty() && size != 0)) {\n                    throw new IllegalStateException(getClass().getName()\n                            + \".sizeOf() is reporting inconsistent results!\");\n                }\n\n                if (size <= maxSize || map.isEmpty()) {\n                    break;\n                }\n\n                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();\n                key = toEvict.getKey();\n                value = toEvict.getValue();\n                map.remove(key);\n                size -= safeSizeOf(key, value);\n                evictionCount++;\n            }\n\n            entryRemoved(true, key, value, null);\n        }\n    }\n\n    /**\n     * Removes the entry for {@code key} if it exists.\n     *\n     * @return the previous value mapped by {@code key}.\n     */\n    public final V remove(K key) {\n        if (key == null) {\n            throw new NullPointerException(\"key == null\");\n        }\n\n        V previous;\n        synchronized (this) {\n            previous = map.remove(key);\n            if (previous != null) {\n                size -= safeSizeOf(key, previous);\n            }\n        }\n\n        if (previous != null) {\n            entryRemoved(false, key, previous, null);\n        }\n\n        return previous;\n    }\n\n    /**\n     * Called for entries that have been evicted or removed. This method is\n     * invoked when a value is evicted to make space, removed by a call to\n     * {@link #remove}, or replaced by a call to {@link #put}. The default\n     * implementation does nothing.\n     *\n     * <p>The method is called without synchronization: other threads may\n     * access the cache while this method is executing.\n     *\n     * @param evicted true if the entry is being removed to make space, false\n     *     if the removal was caused by a {@link #put} or {@link #remove}.\n     * @param newValue the new value for {@code key}, if it exists. If non-null,\n     *     this removal was caused by a {@link #put}. Otherwise it was caused by\n     *     an eviction or a {@link #remove}.\n     */\n    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}\n\n    /**\n     * Called after a cache miss to compute a value for the corresponding key.\n     * Returns the computed value or null if no value can be computed. The\n     * default implementation returns null.\n     *\n     * <p>The method is called without synchronization: other threads may\n     * access the cache while this method is executing.\n     *\n     * <p>If a value for {@code key} exists in the cache when this method\n     * returns, the created value will be released with {@link #entryRemoved}\n     * and discarded. This can occur when multiple threads request the same key\n     * at the same time (causing multiple values to be created), or when one\n     * thread calls {@link #put} while another is creating a value for the same\n     * key.\n     */\n    protected V create(K key) {\n        return null;\n    }\n\n    private long safeSizeOf(K key, V value) {\n        long result = sizeOf(key, value);\n        if (result < 0) {\n            throw new IllegalStateException(\"Negative size: \" + key + \"=\" + value);\n        }\n        return result;\n    }\n\n    /**\n     * Returns the size of the entry for {@code key} and {@code value} in\n     * user-defined units.  The default implementation returns 1 so that size\n     * is the number of entries and max size is the maximum number of entries.\n     *\n     * <p>An entry's size must not change while it is in the cache.\n     */\n    protected long sizeOf(K key, V value) {\n        return 1;\n    }\n\n    /**\n     * Clear the cache, calling {@link #entryRemoved} on each removed entry.\n     */\n    public final void evictAll() {\n        trimToSize(-1); // -1 will evict 0-sized elements\n    }\n\n    /**\n     * For caches that do not override {@link #sizeOf}, this returns the number\n     * of entries in the cache. For all other caches, this returns the sum of\n     * the sizes of the entries in this cache.\n     */\n    public synchronized final long size() {\n        return size;\n    }\n\n    public void setMaxSize(long maxSize) {\n        this.maxSize = maxSize;\n    }\n\n    /**\n     * For caches that do not override {@link #sizeOf}, this returns the maximum\n     * number of entries in the cache. For all other caches, this returns the\n     * maximum sum of the sizes of the entries in this cache.\n     */\n    public synchronized final long maxSize() {\n        return maxSize;\n    }\n\n    /**\n     * Returns the number of times {@link #get} returned a value.\n     */\n    public synchronized final int hitCount() {\n        return hitCount;\n    }\n\n    /**\n     * Returns the number of times {@link #get} returned null or required a new\n     * value to be created.\n     */\n    public synchronized final int missCount() {\n        return missCount;\n    }\n\n    /**\n     * Returns the number of times {@link #create(Object)} returned a value.\n     */\n    public synchronized final int createCount() {\n        return createCount;\n    }\n\n    /**\n     * Returns the number of times {@link #put} was called.\n     */\n    public synchronized final int putCount() {\n        return putCount;\n    }\n\n    /**\n     * Returns the number of values that have been evicted.\n     */\n    public synchronized final int evictionCount() {\n        return evictionCount;\n    }\n\n    /**\n     * Returns a copy of the current contents of the cache, ordered from least\n     * recently accessed to most recently accessed.\n     */\n    public synchronized final Map<K, V> snapshot() {\n        return new LinkedHashMap<K, V>(map);\n    }\n\n    @Override\n    public synchronized final String toString() {\n        int accesses = hitCount + missCount;\n        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;\n        return String.format(Locale.ENGLISH, \"LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]\",\n                maxSize, hitCount, missCount, hitPercent);\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/StreamUtility.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.DataInputStream;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.Channels;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.channels.WritableByteChannel;\n\npublic class StreamUtility {\n    public static void fastChannelCopy(final ReadableByteChannel src, final WritableByteChannel dest) throws IOException {\n        final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);\n        while (src.read(buffer) != -1) {\n            // prepare the buffer to be drained\n            buffer.flip();\n            // write to the channel, may block\n            dest.write(buffer);\n            // If partial transfer, shift remainder down\n            // If buffer is empty, same as doing recycle()\n            buffer.compact();\n        }\n        // EOF will leave buffer in fill state\n        buffer.flip();\n        // make sure the buffer is fully drained.\n        while (buffer.hasRemaining()) {\n            dest.write(buffer);\n        }\n    }\n\n\tpublic static void copyStream(InputStream input, OutputStream output) throws IOException\n\t{\n\t    final ReadableByteChannel inputChannel = Channels.newChannel(input);\n\t    final WritableByteChannel outputChannel = Channels.newChannel(output);\n\t    // copy the channels\n\t    fastChannelCopy(inputChannel, outputChannel);\n\t}\n\n    public static byte[] readToEndAsArray(InputStream input) throws IOException\n    {\n        DataInputStream dis = new DataInputStream(input);\n        byte[] stuff = new byte[1024];\n        ByteArrayOutputStream buff = new ByteArrayOutputStream();\n        int read = 0;\n        while ((read = dis.read(stuff)) != -1)\n        {\n            buff.write(stuff, 0, read);\n        }\n        dis.close();\n        return buff.toByteArray();\n    }\n    \n\tpublic static String readToEnd(InputStream input) throws IOException\n\t{\n\t    return new String(readToEndAsArray(input));\n\t}\n\n    static public String readFile(String filename) throws IOException {\n        return readFile(new File(filename));\n    }\n\n    static public String readFileSilent(String filename) {\n        try {\n            return readFile(new File(filename));\n        }\n        catch (IOException e) {\n            return null;\n        }\n    }\n\n    static public String readFile(File file) throws IOException {\n        byte[] buffer = new byte[(int) file.length()];\n        DataInputStream input = null;\n        try {\n            input = new DataInputStream(new FileInputStream(file));\n            input.readFully(buffer);\n        } finally {\n            closeQuietly(input);\n        }\n        return new String(buffer);\n    }\n    \n    public static void writeFile(File file, String string) throws IOException {\n        file.getParentFile().mkdirs();\n        DataOutputStream dout = new DataOutputStream(new FileOutputStream(file));\n        dout.write(string.getBytes());\n        dout.close();\n    }\n    \n    public static void writeFile(String file, String string) throws IOException {\n        writeFile(new File(file), string);\n    }\n    \n    public static void closeQuietly(Closeable... closeables) {\n        if (closeables == null)\n            return;\n        for (Closeable closeable : closeables) {\n            if (closeable != null) {\n                try {\n                    closeable.close();\n                } catch (Exception e) {\n                    // http://stackoverflow.com/a/156525/9636\n\n                    // also, catch all exceptions because some implementations throw random crap\n                    // like ArrayStoreException\n                }\n            }\n        }\n    }\n\n    public static void eat(InputStream input) throws IOException {\n        byte[] stuff = new byte[1024];\n        while (input.read(stuff) != -1);\n    }\n}\n\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/TaggedList.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.util.ArrayList;\n\npublic class TaggedList<T> extends ArrayList<T> {\n    private Object tag;\n\n    public synchronized <V> V tag() {\n        return (V)tag;\n    }\n\n    public synchronized <V> void tag(V tag) {\n        this.tag = tag;\n    }\n\n    public synchronized <V> void tagNull(V tag) {\n        if (this.tag == null)\n            this.tag = tag;\n    }\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/ThrottleTimeout.java",
    "content": "package com.jeffmony.async.util;\n\nimport android.os.Handler;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.callback.ValueCallback;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by koush on 7/19/16.\n */\npublic class ThrottleTimeout<T> extends TimeoutBase {\n    ValueCallback<List<T>> callback;\n    ArrayList<T> values = new ArrayList<>();\n    ThrottleMode throttleMode = ThrottleMode.Collect;\n\n    public enum ThrottleMode {\n        /**\n         * The timeout will keep resetting until it expires, at which point all\n         * the collected values will be invoked on the callback.\n         */\n        Collect,\n        /**\n         * The callback will be invoked immediately with the first, but future values will be\n         * metered until it expires.\n         */\n        Meter,\n    }\n\n\n    public ThrottleTimeout(final AsyncServer server, long delay, ValueCallback<List<T>> callback) {\n        super(server, delay);\n        this.callback = callback;\n    }\n\n    public ThrottleTimeout(final Handler handler, long delay, ValueCallback<List<T>> callback) {\n        super(handler, delay);\n        this.callback = callback;\n    }\n\n    public void setCallback(ValueCallback<List<T>> callback) {\n        this.callback = callback;\n    }\n\n    private void runCallback() {\n        cancellable = null;\n        ArrayList<T> v = new ArrayList<>(values);\n        values.clear();\n        callback.onResult(v);\n    }\n\n    Object cancellable;\n    public synchronized void postThrottled(final T value) {\n        handlerish.post(() -> {\n            values.add(value);\n\n            if (throttleMode == ThrottleMode.Collect) {\n                // cancel the existing, schedule a new one, and wait.\n                handlerish.removeAllCallbacks(cancellable);\n                cancellable = handlerish.postDelayed(this::runCallback, delay);\n            }\n            else {\n                // nothing is pending, so this can be fired off immediately\n                if (cancellable == null) {\n                    runCallback();\n\n                    // meter future invocations\n                    cancellable = handlerish.postDelayed(this::runCallback, delay);\n                }\n            }\n        });\n    }\n\n    public void setThrottleMode(ThrottleMode throttleMode) {\n        this.throttleMode = throttleMode;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/TimeoutBase.java",
    "content": "package com.jeffmony.async.util;\n\nimport android.os.Handler;\n\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.future.Cancellable;\n\npublic class TimeoutBase {\n    protected Handlerish handlerish;\n    protected long delay;\n\n    interface Handlerish {\n        void post(Runnable r);\n        Object postDelayed(Runnable r, long delay);\n        void removeAllCallbacks(Object cancellable);\n    }\n\n    protected void onCallback() {\n\n    }\n\n    public TimeoutBase(final AsyncServer server, long delay) {\n        this.delay = delay;\n        this.handlerish = new Handlerish() {\n            @Override\n            public void post(Runnable r) {\n                server.post(r);\n            }\n\n            @Override\n            public Object postDelayed(Runnable r, long delay) {\n                return server.postDelayed(r, delay);\n            }\n\n            @Override\n            public void removeAllCallbacks(Object cancellable) {\n                if (cancellable == null)\n                    return;\n                ((Cancellable)cancellable).cancel();\n            }\n        };\n    }\n\n    public TimeoutBase(final Handler handler, long delay) {\n        this.delay = delay;\n        this.handlerish = new Handlerish() {\n            @Override\n            public void post(Runnable r) {\n                handler.post(r);\n            }\n\n            @Override\n            public Object postDelayed(Runnable r, long delay) {\n                handler.postDelayed(r, delay);\n                return r;\n            }\n\n            @Override\n            public void removeAllCallbacks(Object cancellable) {\n                if (cancellable == null)\n                    return;\n                handler.removeCallbacks((Runnable)cancellable);\n            }\n        };\n    }\n\n    public void setDelay(long delay) {\n        this.delay = delay;\n    }\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/util/UntypedHashtable.java",
    "content": "package com.jeffmony.async.util;\n\nimport java.util.Hashtable;\n\npublic class UntypedHashtable {\n    private Hashtable<String, Object> hash = new Hashtable<String, Object>();\n\n    public void put(String key, Object value) {\n        hash.put(key, value);\n    }\n\n    public void remove(String key) {\n        hash.remove(key);\n    }\n\n    public <T> T get(String key, T defaultValue) {\n        T ret = get(key);\n        if (ret == null)\n            return defaultValue;\n        return ret;\n    }\n\n    public <T> T get(String key) {\n        return (T)hash.get(key);\n    }\n}"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/wrapper/AsyncSocketWrapper.java",
    "content": "package com.jeffmony.async.wrapper;\n\nimport com.jeffmony.async.AsyncSocket;\n\npublic interface AsyncSocketWrapper extends AsyncSocket, DataEmitterWrapper {\n    AsyncSocket getSocket();\n}\n"
  },
  {
    "path": "androidasync/src/main/java/com/jeffmony/async/wrapper/DataEmitterWrapper.java",
    "content": "package com.jeffmony.async.wrapper;\n\nimport com.jeffmony.async.DataEmitter;\n\npublic interface DataEmitterWrapper extends DataEmitter {\n    DataEmitter getDataEmitter();\n}\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion \"29.0.2\"\n\n    defaultConfig {\n        applicationId \"com.android.media\"\n        minSdkVersion 19\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'\n    implementation 'com.github.JeffMony:ORCodeDemo:v1.3.0'\n    implementation 'com.google.zxing:core:3.3.3'\n\n    implementation project(path: ':playersdk')\n    implementation project(path: ':mediaproxy')\n    implementation project(path: ':base')\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.media\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <application\n        android:name=\".MyApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:usesCleartextTraffic=\"true\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:theme=\"@style/AppTheme\"\n        android:supportsRtl=\"true\">\n        <activity android:name=\".MainActivity\"\n            android:windowSoftInputMode=\"adjustUnspecified|stateHidden\"\n            android:configChanges=\"orientation|keyboardHidden\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <activity android:name=\".PlayFeatureActivity\"\n            android:windowSoftInputMode=\"adjustUnspecified|stateHidden\"\n            android:configChanges=\"orientation|keyboardHidden\"/>\n\n        <activity android:name=\".DownloadFeatureActivity\" />\n\n        <activity android:name=\".DownloadSettingsActivity\" />\n\n        <activity android:name=\".DownloadBaseListActivity\" />\n\n        <activity android:name=\".DownloadOrcodeActivity\" />\n\n        <activity android:name=\".MediaScannerActivity\" />\n        \n        <activity android:name=\".PlayerActivity\" />\n\n        <activity android:name=\".DownloadPlayActivity\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/assets/list.json",
    "content": "[\n  {\n    \"name\": \"Test url 1\",\n    \"uri\": \"https://tv.youkutv.cc/2019/10/28/6MSVuLec4zbpYFlj/playlist.m3u8\"\n  },\n  {\n    \"name\": \"Test url 2\",\n    \"uri\": \"https://kuku.zuida-youku.com/20170616/cBIBaYMJ/index.m3u8\"\n  },\n  {\n    \"name\": \"Test url 3\",\n    \"uri\": \"https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8\"\n  },\n  {\n    \"name\": \"Test url 4\",\n    \"uri\": \"https://tv.youkutv.cc/2020/01/15/3d97sO5xQUYB5bvY/playlist.m3u8\"\n  },\n  {\n    \"name\": \"Test url 5\",\n    \"uri\": \"https://hls.aoxtv.com/v3.szjal.cn/20200122/TIj9Ekt9/index.m3u8\"\n  },\n  {\n    \"name\": \"Test url 6\",\n    \"uri\": \"https://hls.aoxtv.com/v3.szjal.cn/20200114/dtOHlPFE/index.m3u8\"\n  },\n  {\n    \"name\": \"Test url 7\",\n    \"uri\": \"https://hls.aoxtv.com/v3.szjal.cn/20200115/qNIba0qo/index.m3u8\"\n  },\n  {\n    \"name\": \"Test url 8\",\n    \"uri\": \"https://m3u8.soyoung.com/9d0e36edc95edfa34fa049c3bfedf2e1_7d68ac41_new22.m3u8?sign=7f717d032fb7a53b4c628c22d7e9a206&t=5f0000fd\"\n  }\n]"
  },
  {
    "path": "app/src/main/java/com/android/media/DownloadBaseListActivity.java",
    "content": "package com.android.media;\n\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.Button;\nimport android.widget.ListView;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.model.Video;\nimport com.media.cache.VideoDownloadManager;\nimport com.media.cache.listener.IDownloadListener;\nimport com.media.cache.model.VideoTaskItem;\nimport com.media.cache.model.VideoTaskMode;\n\npublic class DownloadBaseListActivity extends AppCompatActivity {\n\n    private ListView mDownloadListView;\n    private TextView mFilePath;\n    private Button mClearBtn;\n    private Button mPauseBtn;\n\n    private VideoListAdapter mAdapter;\n    private VideoTaskItem[] items = new VideoTaskItem[8];\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_download_list);\n\n        VideoDownloadManager.getInstance().setGlobalDownloadListener(mListener);\n        initViews();\n        initDatas();\n    }\n\n    private void initViews() {\n        mDownloadListView = (ListView) findViewById(R.id.download_listview);\n        mFilePath = (TextView) findViewById(R.id.file_path);\n        mClearBtn = (Button) findViewById(R.id.clear_cache_btn);\n        mPauseBtn = (Button) findViewById(R.id.pause_task_btn);\n        mFilePath.setText(VideoDownloadManager.getInstance().getCacheFilePath());\n    }\n\n    private void initDatas() {\n        VideoTaskItem item1 = new VideoTaskItem(\"https://tv.youkutv.cc/2019/10/28/6MSVuLec4zbpYFlj/playlist.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item2 = new VideoTaskItem(\"https://kuku.zuida-youku.com/20170616/cBIBaYMJ/index.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item3 = new VideoTaskItem(\"https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item4 = new VideoTaskItem(\"https://tv.youkutv.cc/2020/01/15/3d97sO5xQUYB5bvY/playlist.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item5 = new VideoTaskItem(\"https://hls.aoxtv.com/v3.szjal.cn/20200122/TIj9Ekt9/index.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item6 = new VideoTaskItem(\"https://hls.aoxtv.com/v3.szjal.cn/20200114/dtOHlPFE/index.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item7 = new VideoTaskItem(\"https://hls.aoxtv.com/v3.szjal.cn/20200115/qNIba0qo/index.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n        VideoTaskItem item8 = new VideoTaskItem(\"https://hls.aoxtv.com/v3.szjal.cn/20200114/2KwuUDMK/index.m3u8\", VideoTaskMode.DOWNLOAD_MODE);\n\n        items[0] = item1;\n        items[1] = item2;\n        items[2] = item3;\n        items[3] = item4;\n        items[4] = item5;\n        items[5] = item6;\n        items[6] = item7;\n        items[7] = item8;\n\n        mAdapter = new VideoListAdapter(this, R.layout.download_item, items);\n        mDownloadListView.setAdapter(mAdapter);\n\n        mDownloadListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {\n            @Override\n            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {\n                LogUtils.d(\"jeffmony onItemClick url=\"+items[position].getUrl());\n                VideoTaskItem item = items[position];\n                if (item.isRunningTask()) {\n                    LogUtils.d(\"jeffmony pause downloading.\");\n                    VideoDownloadManager.getInstance().pauseDownloadTask(item);\n                } else if (item.isSlientTask()) {\n                    LogUtils.d(\"jeffmony start downloading.\");\n                    VideoDownloadManager.getInstance().startDownload(item);\n                }\n            }\n        });\n\n        mDownloadListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {\n            @Override\n            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {\n                LogUtils.w(\"jeffmony long click\");\n                return true;\n            }\n        });\n\n        mClearBtn.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                VideoDownloadManager.getInstance().deleteVideoTasks(items);\n            }\n        });\n\n        mPauseBtn.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                VideoDownloadManager.getInstance().pauseDownloadTasks(items);\n            }\n        });\n    }\n\n    private IDownloadListener mListener = new IDownloadListener() {\n\n\n        @Override\n        public void onDownloadDefault(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadDefault: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadPending(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadPending: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadPrepare(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadPrepare: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadStart(VideoTaskItem item) {\n            LogUtils.d(\"onDownloadStart: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadProxyReady(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadProxyReady: \" + item.getProxyUrl());\n        }\n\n        @Override\n        public void onDownloadProgress(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadProgress: \" + item.getPercentString());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadSpeed(VideoTaskItem item) {\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadPause(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadPause: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadError(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadError: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadProxyForbidden(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadForbidden: \" + item.getUrl());\n            notifyChanged(item);\n        }\n\n        @Override\n        public void onDownloadSuccess(VideoTaskItem item) {\n            LogUtils.d(\"jeffmony onDownloadSuccess: \" + item.getUrl());\n            notifyChanged(item);\n        }\n    };\n\n    private void notifyChanged(VideoTaskItem item) {\n        runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                mAdapter.notifyChanged(items, item);\n            }\n        });\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        LogUtils.w(\"jeffmony onDestroy\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/DownloadFeatureActivity.java",
    "content": "package com.android.media;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic class DownloadFeatureActivity extends AppCompatActivity implements View.OnClickListener {\n\n    private Button mDownloadConfigBtn;\n    private Button mDownloadBaseBtn;\n    private Button mDownloadOrcodeBtn;\n    private Button mCurrentDownloadBtn;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_download_feature);\n\n        initViews();\n    }\n\n    private void initViews() {\n        mDownloadConfigBtn = (Button) findViewById(R.id.download_settings_btn);\n        mDownloadBaseBtn = (Button) findViewById(R.id.download_normal_btn);\n        mDownloadOrcodeBtn = (Button) findViewById(R.id.download_orcode);\n        mCurrentDownloadBtn = (Button) findViewById(R.id.list_download_btn);\n\n        mDownloadConfigBtn.setOnClickListener(this);\n        mDownloadBaseBtn.setOnClickListener(this);\n        mDownloadOrcodeBtn.setOnClickListener(this);\n        mCurrentDownloadBtn.setOnClickListener(this);\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v == mDownloadConfigBtn) {\n            Intent intent = new Intent(DownloadFeatureActivity.this, DownloadSettingsActivity.class);\n            startActivity(intent);\n        } else if (v == mDownloadBaseBtn) {\n            Intent intent = new Intent(DownloadFeatureActivity.this, DownloadBaseListActivity.class);\n            startActivity(intent);\n        } else if (v == mDownloadOrcodeBtn) {\n            Intent intent = new Intent(DownloadFeatureActivity.this, DownloadOrcodeActivity.class);\n            startActivity(intent);\n        } else if (v == mCurrentDownloadBtn) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/DownloadOrcodeActivity.java",
    "content": "package com.android.media;\n\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.EditText;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic class DownloadOrcodeActivity extends AppCompatActivity implements View.OnClickListener {\n\n    private EditText mDownloadUrlText;\n    private Button mSingleDownloadBtn;\n    private Button mOrcodeScannerBtn;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_orcode);\n\n        initViews();\n    }\n\n    private void initViews() {\n        mDownloadUrlText = (EditText) findViewById(R.id.download_url_text);\n        mSingleDownloadBtn = (Button) findViewById(R.id.download_single_btn);\n        mOrcodeScannerBtn = (Button) findViewById(R.id.orcode_scanner_btn);\n\n        mSingleDownloadBtn.setOnClickListener(this);\n        mOrcodeScannerBtn.setOnClickListener(this);\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v == mSingleDownloadBtn) {\n\n        } else if (v == mOrcodeScannerBtn) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/DownloadPlayActivity.java",
    "content": "package com.android.media;\n\nimport android.graphics.SurfaceTexture;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.view.Surface;\nimport android.view.TextureView;\nimport android.view.View;\nimport android.widget.ImageButton;\nimport android.widget.LinearLayout;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.android.baselib.utils.ScreenUtils;\nimport com.android.baselib.utils.Utility;\nimport com.android.player.CommonPlayer;\nimport com.android.player.IPlayer;\nimport com.android.player.PlayerAttributes;\nimport com.android.player.PlayerType;\nimport com.media.cache.model.Video;\nimport com.media.cache.model.VideoTaskMode;\n\nimport java.io.IOException;\n\npublic class DownloadPlayActivity extends AppCompatActivity implements View.OnClickListener {\n\n    private String mProxyUrl;\n    private String mOriginUrl;\n\n    private TextureView mVideoView;\n    private ImageButton mControlBtn;\n    private TextView mTimeView;\n    private SeekBar mProgressView;\n    private CommonPlayer mPlayer;\n    private Surface mSurface;\n\n    private int mSurfaceWidth;\n    private int mSurfaceHeight;\n    private int mVideoWidth;\n    private int mVideoHeight;\n    private float mPixelRatio; //SAR\n    private long mDuration = 0L;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_download_play);\n\n        mProxyUrl = getIntent().getStringExtra(\"proxy_url\");\n        mOriginUrl = getIntent().getStringExtra(\"origin_url\");\n\n        mSurfaceWidth = ScreenUtils.getScreenWidth(this);\n        initViews();\n\n    }\n\n    private void initViews() {\n        mVideoView = (TextureView) findViewById(R.id.download_video_view);\n        mProgressView = (SeekBar) findViewById(R.id.download_progress_view);\n        mControlBtn = (ImageButton) findViewById(R.id.download_control_btn);\n        mTimeView = (TextView) findViewById(R.id.download_time_view);\n\n        mVideoView.setSurfaceTextureListener(mSurfaceTextureListener);\n        mControlBtn.setOnClickListener(this);\n        mProgressView.setOnSeekBarChangeListener(mSeekBarChangeListener);\n    }\n\n    private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {\n        @Override\n        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {\n            mSurface = new Surface(surface);\n            initPlayer();\n        }\n\n        @Override\n        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {\n\n        }\n\n        @Override\n        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {\n            return true;\n        }\n\n        @Override\n        public void onSurfaceTextureUpdated(SurfaceTexture surface) {\n\n        }\n    };\n\n    private void initPlayer() {\n\n        PlayerAttributes attributes = new PlayerAttributes(\"\");\n        attributes.setVideoCacheSwitch(true);\n        attributes.setTaskMode(VideoTaskMode.DOWNLOAD_PLAY_MODE);\n\n        mPlayer = new CommonPlayer(this, PlayerType.EXO_PLAYER, attributes);\n        Uri uri = Uri.parse(mProxyUrl);\n        try {\n            mPlayer.setDataSource(DownloadPlayActivity.this, uri);\n        } catch (IOException e) {\n            e.printStackTrace();\n            return;\n        }\n        mPlayer.setOriginUrl(mOriginUrl);\n        mPlayer.setSurface(mSurface);\n        mPlayer.setOnPreparedListener(mPreparedListener);\n        mPlayer.setOnErrorListener(mErrorListener);\n        mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener);\n        mPlayer.setOnLocalProxyCacheListener(mProxyCacheListener);\n        mPlayer.prepareAsync();\n    }\n\n    private IPlayer.OnPreparedListener mPreparedListener = new IPlayer.OnPreparedListener() {\n        @Override\n        public void onPrepared(IPlayer mp) {\n            doPlayVideo();\n        }\n    };\n\n    private IPlayer.OnErrorListener mErrorListener = new IPlayer.OnErrorListener() {\n        @Override\n        public void onError(IPlayer mp, int what, String msg) {\n            Toast.makeText(DownloadPlayActivity.this, \"Play Error\", Toast.LENGTH_SHORT).show();\n        }\n    };\n\n    private IPlayer.OnVideoSizeChangedListener mVideoSizeChangeListener = new IPlayer.OnVideoSizeChangedListener() {\n\n        @Override\n        public void onVideoSizeChanged(IPlayer mp, int width, int height, int rotationDegree, float pixelRatio, float darRatio) {\n\n            LogUtils.d(\"PlayerActivity onVideoSizeChanged width=\"+width+\", height=\"+height + \", pixedlRatio = \" + pixelRatio);\n            mVideoWidth = width;\n            mVideoHeight = height;\n            mPixelRatio = pixelRatio;\n            mSurfaceHeight = (int)(mSurfaceWidth * mVideoHeight * 1.0f / mVideoWidth);\n\n            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mSurfaceWidth, mSurfaceHeight);\n            mVideoView.setLayoutParams(params);\n        }\n    };\n\n    @Override\n    public void onClick(View view) {\n        LogUtils.e(\"click event\");\n        if(view == mControlBtn) {\n            if (!mPlayer.isPlaying()) {\n                mPlayer.start();\n                mControlBtn.setImageResource(R.mipmap.played_state);\n            } else {\n                mPlayer.pause();\n                mControlBtn.setImageResource(R.mipmap.paused_state);\n            }\n        }\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        if (mPlayer != null) {\n            mPlayer.pause();\n            mControlBtn.setImageResource(R.mipmap.paused_state);\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        doReleasePlayer();\n    }\n\n    private void doPlayVideo() {\n        if (mPlayer != null) {\n            mTimeView.setVisibility(View.VISIBLE);\n            mPlayer.start();\n            mDuration = mPlayer.getDuration();\n            mControlBtn.setImageResource(R.mipmap.played_state);\n            mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);\n        }\n    }\n\n    private void doReleasePlayer() {\n        if (mPlayer != null) {\n            mPlayer.stop();\n            mPlayer.release();\n            mPlayer = null;\n        }\n    }\n\n    private static final int MSG_UPDATE_PROGRESS = 0x1;\n    private static final int MAX_PROGRESS = 1000;\n\n    private Handler mHandler = new Handler() {\n\n        @Override\n        public void handleMessage(Message msg) {\n            if (msg.what == MSG_UPDATE_PROGRESS) {\n                updateProgressView();\n            }\n        }\n    };\n\n    private void updateProgressView() {\n        if (mPlayer != null) {\n            long currentPosition = mPlayer.getCurrentPosition();\n            mTimeView.setText(Utility.getVideoTimeString(currentPosition) + \" / \" + Utility.getVideoTimeString(mDuration));\n//            mProgressView.setProgress((int)(1000 *  currentPosition * 1.0f / mDuration));\n//            int cacheProgress = (int)(mPercent * 1.0f / 100 * 1000);\n//            mProgressView.setSecondaryProgress(cacheProgress);\n        }\n        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, 1000);\n    }\n\n    private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {\n        @Override\n        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n        }\n\n        @Override\n        public void onStartTrackingTouch(SeekBar seekBar) {\n            if (mPlayer != null) {\n                mHandler.removeMessages(MSG_UPDATE_PROGRESS);\n            }\n            LogUtils.d(\"onStartTrackingTouch progress=\"+mProgressView.getProgress());\n        }\n\n        @Override\n        public void onStopTrackingTouch(SeekBar seekBar) {\n            LogUtils.d(\"onStopTrackingTouch progress=\"+mProgressView.getProgress());\n\n            if (mPlayer != null) {\n                int progress = mProgressView.getProgress();\n                int seekPosition = (int)(progress * 1.0f / MAX_PROGRESS * mDuration);\n                mPlayer.seekTo(seekPosition);\n\n                mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);\n            }\n        }\n    };\n\n    private IPlayer.OnLocalProxyCacheListener mProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() {\n        @Override\n        public void onCacheReady(IPlayer mp, String proxyUrl) {\n\n        }\n\n        @Override\n        public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) {\n\n        }\n\n        @Override\n        public void onCacheSpeedChanged(IPlayer mp, float speed) {\n\n        }\n\n        @Override\n        public void onCacheForbidden(IPlayer mp, String url) {\n\n        }\n\n        @Override\n        public void onCacheFinished(IPlayer mp) {\n\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/DownloadSettingsActivity.java",
    "content": "package com.android.media;\n\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.view.View;\nimport android.widget.RadioButton;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.android.baselib.utils.Utility;\nimport com.media.cache.VideoDownloadManager;\nimport com.media.cache.utils.StorageUtils;\n\nimport java.io.File;\n\npublic class DownloadSettingsActivity\n        extends AppCompatActivity implements View.OnClickListener {\n\n    private static final int MSG_COUNT_SIZE = 0x1;\n    private TextView mStoreLocText;\n    private TextView mStoreSizeText;\n    private TextView mOpenFileText;\n    private TextView mClearDownloadText;\n    private RadioButton mBtn1;\n    private RadioButton mBtn2;\n    private RadioButton mBtn3;\n    private RadioButton mBtn4;\n    private RadioButton mBtn5;\n    private RadioButton mBtn11;\n    private RadioButton mBtn12;\n\n    private int mConcurrentNum = 3;\n    private boolean mIgnoreCertErrors = true;\n\n    private Handler mHandler = new Handler() {\n        @Override\n        public void handleMessage(Message msg) {\n            if (msg.what == MSG_COUNT_SIZE) {\n                String filePath = VideoDownloadManager.getInstance().getCacheFilePath();\n                File file = new File(filePath);\n                if (file.exists()) {\n                    long size = StorageUtils.countTotalSize(file);\n                    mStoreSizeText.setText(Utility.getSize(size));\n                }\n            }\n        }\n    };\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_download_settings);\n        initViews();\n    }\n\n    private void initViews() {\n        mStoreLocText = (TextView)findViewById(R.id.store_loc_txt);\n        mStoreSizeText = (TextView)findViewById(R.id.store_size);\n        mOpenFileText = (TextView)findViewById(R.id.open_file_txt);\n        mClearDownloadText = (TextView)findViewById(R.id.clear_download_cache);\n        mBtn1 = (RadioButton)findViewById(R.id.btn1);\n        mBtn2 = (RadioButton)findViewById(R.id.btn2);\n        mBtn3 = (RadioButton)findViewById(R.id.btn3);\n        mBtn4 = (RadioButton)findViewById(R.id.btn4);\n        mBtn5 = (RadioButton)findViewById(R.id.btn5);\n        mBtn11 = (RadioButton)findViewById(R.id.btn11);\n        mBtn12 = (RadioButton)findViewById(R.id.btn12);\n\n        mStoreLocText.setText(\n                VideoDownloadManager.getInstance().getCacheFilePath());\n        mOpenFileText.setOnClickListener(this);\n        mClearDownloadText.setOnClickListener(this);\n        mBtn1.setOnClickListener(this);\n        mBtn2.setOnClickListener(this);\n        mBtn3.setOnClickListener(this);\n        mBtn4.setOnClickListener(this);\n        mBtn5.setOnClickListener(this);\n        mBtn11.setOnClickListener(this);\n        mBtn12.setOnClickListener(this);\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        mHandler.sendEmptyMessage(MSG_COUNT_SIZE);\n        checkBtnState(VideoDownloadManager.getInstance()\n                .downloadConfig()\n                .getConcurrentCount());\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v == mClearDownloadText) {\n            VideoDownloadManager.getInstance().deleteAllVideoFiles(\n                    DownloadSettingsActivity.this);\n            mHandler.sendEmptyMessage(MSG_COUNT_SIZE);\n        } else if (v == mOpenFileText) {\n            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n            intent.setDataAndType(\n                    Uri.parse(VideoDownloadManager.getInstance().getCacheFilePath()),\n                    \"file/*\");\n            intent.addCategory(Intent.CATEGORY_OPENABLE);\n            startActivity(intent);\n        } else if (v == mBtn1) {\n            checkBtnState(1);\n        } else if (v == mBtn2) {\n            checkBtnState(2);\n        } else if (v == mBtn3) {\n            checkBtnState(3);\n        } else if (v == mBtn4) {\n            checkBtnState(4);\n        } else if (v == mBtn5) {\n            checkBtnState(5);\n        } else if (v == mBtn11) {\n            mBtn11.setChecked(true);\n            mBtn12.setChecked(false);\n            mIgnoreCertErrors = true;\n        } else if (v == mBtn12) {\n            mBtn11.setChecked(false);\n            mBtn12.setChecked(true);\n            mIgnoreCertErrors = false;\n        }\n        VideoDownloadManager.getInstance().setConcurrentCount(mConcurrentNum);\n        VideoDownloadManager.getInstance().setIgnoreAllCertErrors(\n                mIgnoreCertErrors);\n    }\n\n    private void checkBtnState(int type) {\n        if (type == 1) {\n            mBtn1.setChecked(true);\n            mBtn2.setChecked(false);\n            mBtn3.setChecked(false);\n            mBtn4.setChecked(false);\n            mBtn5.setChecked(false);\n        } else if (type == 2) {\n            mBtn1.setChecked(false);\n            mBtn2.setChecked(true);\n            mBtn3.setChecked(false);\n            mBtn4.setChecked(false);\n            mBtn5.setChecked(false);\n        } else if (type == 3) {\n            mBtn1.setChecked(false);\n            mBtn2.setChecked(false);\n            mBtn3.setChecked(true);\n            mBtn4.setChecked(false);\n            mBtn5.setChecked(false);\n        } else if (type == 4) {\n            mBtn1.setChecked(false);\n            mBtn2.setChecked(false);\n            mBtn3.setChecked(false);\n            mBtn4.setChecked(true);\n            mBtn5.setChecked(false);\n        } else if (type == 5) {\n            mBtn1.setChecked(false);\n            mBtn2.setChecked(false);\n            mBtn3.setChecked(false);\n            mBtn4.setChecked(false);\n            mBtn5.setChecked(true);\n        }\n        mConcurrentNum = type;\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/android/media/MainActivity.java",
    "content": "package com.android.media;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic class MainActivity extends AppCompatActivity implements View.OnClickListener {\n\n    private Button mPlayBtn;\n    private Button mDownloadBtn;\n    private Button mScanBtn;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n\n        mPlayBtn = (Button) findViewById(R.id.play_btn);\n        mDownloadBtn = (Button) findViewById(R.id.download_btn);\n        mScanBtn = (Button) findViewById(R.id.scan_btn);\n        mPlayBtn.setOnClickListener(this);\n        mDownloadBtn.setOnClickListener(this);\n        mScanBtn.setOnClickListener(this);\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v == mPlayBtn) {\n            Intent intent = new Intent(this, PlayFeatureActivity.class);\n            startActivity(intent);\n        } else if (v == mDownloadBtn) {\n            Intent intent = new Intent(this, DownloadFeatureActivity.class);\n            startActivity(intent);\n        } else if (v == mScanBtn) {\n            Intent intent = new Intent(this, MediaScannerActivity.class);\n            startActivity(intent);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/MediaScannerActivity.java",
    "content": "package com.android.media;\n\nimport android.os.Bundle;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic class MediaScannerActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_scanner);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/MyApplication.java",
    "content": "package com.android.media;\n\nimport android.app.Application;\n\nimport com.media.cache.DownloadConstants;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.VideoDownloadManager;\nimport com.media.cache.utils.StorageUtils;\n\nimport java.io.File;\n\npublic class MyApplication extends Application {\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        File file = StorageUtils.getVideoCacheDir(this);\n        if (!file.exists()) {\n            file.mkdir();\n        }\n        LocalProxyConfig config = new VideoDownloadManager.Build(this)\n                        .setCacheRoot(file)\n                        .setUrlRedirect(false)\n                        .setTimeOut(DownloadConstants.READ_TIMEOUT,\n                                DownloadConstants.CONN_TIMEOUT,\n                                DownloadConstants.SOCKET_TIMEOUT)\n                        .setConcurrentCount(DownloadConstants.CONCURRENT_COUNT)\n                        .setIgnoreAllCertErrors(true)\n                        .buildConfig();\n        VideoDownloadManager.getInstance().initConfig(config);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/PlayFeatureActivity.java",
    "content": "package com.android.media;\n\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.os.Bundle;\nimport android.os.Environment;\nimport android.text.TextUtils;\nimport android.util.JsonReader;\nimport android.util.JsonToken;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.Button;\nimport android.widget.CheckBox;\nimport android.widget.CompoundButton;\nimport android.widget.EditText;\nimport android.widget.LinearLayout;\nimport android.widget.ListView;\nimport android.widget.RadioButton;\nimport android.widget.RadioGroup;\nimport android.widget.SimpleAdapter;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.CacheManager;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.content.ContextCompat;\n\npublic class PlayFeatureActivity extends AppCompatActivity implements View.OnClickListener, RadioGroup.OnCheckedChangeListener {\n\n    private static final String WRITE_EXTERNAL_STORAGE = \"android.permission.WRITE_EXTERNAL_STORAGE\";\n    private static final int REQUEST_PERMISSION_OK = 0x1;\n\n    private EditText mVideoUrlView;\n    private Button mPlayBtn;\n    private ListView mVideoListView;\n\n    private RadioGroup mPlayerBtnGroup;\n    private RadioButton mIjkPlayerBtn;\n    private RadioButton mExoPlayerBtn;\n\n    private CheckBox mVideoCacheBox;\n    private TextView mCachedLocationView;\n    private TextView mCacheSizeView;\n    private TextView mClearCacheView;\n\n    private List<HashMap<String, String>> mVideoList;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_play_func);\n\n        initViews();\n        initViewListData();\n    }\n\n    private void initViews() {\n        mVideoUrlView = (EditText) findViewById(R.id.video_url_view);\n        mPlayBtn = (Button) findViewById(R.id.play_btn);\n        mVideoListView = (ListView) findViewById(R.id.video_list);\n        mPlayerBtnGroup = (RadioGroup) findViewById(R.id.play_btn_group);\n        mIjkPlayerBtn = (RadioButton) findViewById(R.id.ijkplayer_btn);\n        mExoPlayerBtn = (RadioButton) findViewById(R.id.exoplayer_btn);\n        mVideoCacheBox = (CheckBox) findViewById(R.id.local_proxy_box);\n        mCachedLocationView = (TextView) findViewById(R.id.cached_location_view);\n        mCacheSizeView = (TextView) findViewById(R.id.cache_size_view);\n        mClearCacheView = (TextView) findViewById(R.id.clear_cache_view);\n\n        mExoPlayerBtn.setChecked(true);\n\n        mPlayBtn.setOnClickListener(this);\n        mClearCacheView.setOnClickListener(this);\n\n        mPlayerBtnGroup.setOnCheckedChangeListener(this);\n\n        mCachedLocationView.setText(CacheManager.getCachePath());\n    }\n\n    private void initViewListData() {\n        mVideoList = new ArrayList<>();\n        try {\n            InputStream is = null;\n            try {\n                String PATH = Environment.getExternalStorageDirectory() + \"/list.json\";\n                File file = new File(PATH);\n\n                if (file.exists()) {\n                    is = new FileInputStream(PATH);\n                } else {\n                    is = getAssets().open(\"list.json\");\n                }\n                JsonReader reader = new JsonReader(new InputStreamReader(is));\n                reader.beginArray();\n                while (reader.hasNext()) {\n                    reader.beginObject();\n                    HashMap<String, String> item = new HashMap<>();\n                    while (reader.hasNext()) {\n                        String name = reader.nextName();\n                        if (name.equals(\"name\")) {\n                            String videoName = reader.nextString();\n                            item.put(\"name\", videoName);\n                        } else if (name.equals(\"age\") || reader.peek() != JsonToken.NULL) { // 当前获取的字段是否为：null\n                            String videoUrl = reader.nextString();\n                            item.put(\"url\", videoUrl);\n                        }\n                    }\n                    reader.endObject();\n                    mVideoList.add(item);\n                }\n                reader.endArray();\n            } finally {\n                if (null != is) {\n                    is.close();\n                }\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        SimpleAdapter adapter = new SimpleAdapter(this, mVideoList, R.layout.video_item, new String[]{\"name\"}, new int[]{R.id.video_name});\n        mVideoListView.setAdapter(adapter);\n        mVideoListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {\n\n            @Override\n            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {\n                String url = mVideoList.get(position).get(\"url\");\n                mVideoUrlView.setText(url);\n            }\n        });\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        checkPermission();\n\n        mCacheSizeView.setText(CacheManager.getCachedSize());\n    }\n\n    private void checkPermission() {\n        if (ContextCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE)\n                != PackageManager.PERMISSION_GRANTED) {\n            ActivityCompat.requestPermissions(this, new String[] { WRITE_EXTERNAL_STORAGE }, REQUEST_PERMISSION_OK);\n        }\n    }\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\n        if (requestCode == REQUEST_PERMISSION_OK) {\n            if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n                Toast.makeText(this,  \"存储权限已开通\", Toast.LENGTH_SHORT).show();\n            } else {\n                Toast.makeText(this,  \"存储权限被拒绝\", Toast.LENGTH_SHORT).show();\n            }\n        }\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v == mPlayBtn) {\n            doPlayVideo();\n        } else if (v == mClearCacheView) {\n            clearVideoCache();\n        }\n    }\n\n    @Override\n    public void onCheckedChanged(RadioGroup group, int checkedId) {\n        LogUtils.d(\"onCheckedChanged checkedId = \" + checkedId);\n    }\n\n    private void doPlayVideo() {\n        String url = mVideoUrlView.getText().toString();\n        if (TextUtils.isEmpty(url)) {\n            Toast.makeText(this,  \"输入的url为空\", Toast.LENGTH_SHORT).show();\n        } else {\n            Intent intent = new Intent(this, PlayerActivity.class);\n            intent.putExtra(\"url\", url);\n\n            int playerType = -1;\n            if (mIjkPlayerBtn.isChecked()) {\n                playerType = 1;\n            } else if (mExoPlayerBtn.isChecked()) {\n                playerType = 2;\n            }\n            intent.putExtra(\"playerType\", playerType);\n            boolean videoCached = mVideoCacheBox.isChecked();\n            intent.putExtra(\"videoCached\", videoCached);\n\n            startActivity(intent);\n        }\n    }\n\n    private void clearVideoCache() {\n        CacheManager.deleteCacheFile();\n        mCacheSizeView.setText(\"0 MB\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/PlayerActivity.java",
    "content": "package com.android.media;\n\nimport android.graphics.SurfaceTexture;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.view.Gravity;\nimport android.view.Surface;\nimport android.view.TextureView;\nimport android.view.View;\nimport android.widget.ImageButton;\nimport android.widget.LinearLayout;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.android.baselib.utils.Utility;\nimport com.android.baselib.utils.ScreenUtils;\nimport com.android.baselib.utils.LogUtils;\nimport com.android.player.CommonPlayer;\nimport com.android.player.IPlayer;\nimport com.android.player.PlayerAttributes;\nimport com.android.player.PlayerType;\n\nimport java.io.IOException;\n\npublic class PlayerActivity extends AppCompatActivity implements View.OnClickListener {\n\n    private TextureView mVideoView;\n    private ImageButton mControlBtn;\n    private TextView mTimeView;\n    private SeekBar mProgressView;\n    private TextView mPlayTipView;\n    private int mSurfaceWidth;\n    private int mSurfaceHeight;\n    private int mVideoWidth;\n    private int mVideoHeight;\n    private float mPixelRatio; //SAR\n    private float mDarRatio;\n    private long mDuration = 0L;\n\n    private CommonPlayer mPlayer;\n    private Surface mSurface;\n    private String mUrl = \"https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8\";\n    private int mPlayerType = -1;\n    private boolean mVideoCached = false;\n    private int mPercent = 0;\n    private long mCacheSize = 0L;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_player);\n        getWindow().setFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,\n                android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n        mUrl = getIntent().getStringExtra(\"url\");\n        mPlayerType = getIntent().getIntExtra(\"playerType\", -1);\n        if (mPlayerType == -1) {\n            mPlayerType = 1;\n        }\n        mVideoCached = getIntent().getBooleanExtra(\"videoCached\", false);\n        mSurfaceWidth = ScreenUtils.getScreenWidth(this);\n        initViews();\n    }\n\n    private void initViews() {\n        mVideoView = (TextureView) findViewById(R.id.video_view);\n        mTimeView = (TextView) findViewById(R.id.video_time_view);\n        mProgressView = (SeekBar) findViewById(R.id.video_progress_view);\n        mControlBtn = (ImageButton) findViewById(R.id.video_control_btn);\n        mPlayTipView = (TextView) findViewById(R.id.play_tip_view);\n\n        mControlBtn.setOnClickListener(this);\n        mVideoView.setSurfaceTextureListener(mSurfaceTextureListener);\n        mProgressView.setOnSeekBarChangeListener(mSeekBarChangeListener);\n    }\n\n    private void initPlayer() {\n\n        PlayerAttributes attributes = new PlayerAttributes(\"\");\n        attributes.setVideoCacheSwitch(mVideoCached);\n\n        if (mPlayerType == 1) {\n            mPlayer = new CommonPlayer(this, PlayerType.IJK_PLAYER, attributes);\n        } else if (mPlayerType == 2) {\n            mPlayer = new CommonPlayer(this, PlayerType.EXO_PLAYER, attributes);\n        } else if (mPlayerType == 3) {\n            mPlayer = new CommonPlayer(this, PlayerType.MEDIA_PLAYER, attributes);\n        }\n\n        if (mVideoCached) {\n            mPlayer.setOnLocalProxyCacheListener(mOnLocalProxyCacheListener);\n            mPlayer.startLocalProxy(mUrl);\n        } else {\n            Uri uri = Uri.parse(mUrl);\n            try {\n                mPlayer.setDataSource(PlayerActivity.this, uri);\n            } catch (IOException e) {\n                e.printStackTrace();\n                return;\n            }\n            mPlayer.setSurface(mSurface);\n            mPlayer.setOnPreparedListener(mPreparedListener);\n            mPlayer.setOnErrorListener(mErrorListener);\n            mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener);\n            mPlayer.prepareAsync();\n        }\n    }\n\n    private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {\n        @Override\n        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {\n            mSurface = new Surface(surface);\n            initPlayer();\n        }\n\n        @Override\n        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {\n\n        }\n\n        @Override\n        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {\n            return true;\n        }\n\n        @Override\n        public void onSurfaceTextureUpdated(SurfaceTexture surface) {\n\n        }\n    };\n\n    private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {\n        @Override\n        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n        }\n\n        @Override\n        public void onStartTrackingTouch(SeekBar seekBar) {\n            if (mPlayer != null) {\n                mHandler.removeMessages(MSG_UPDATE_PROGRESS);\n            }\n            LogUtils.d(\"onStartTrackingTouch progress=\"+mProgressView.getProgress());\n        }\n\n        @Override\n        public void onStopTrackingTouch(SeekBar seekBar) {\n            LogUtils.d(\"onStopTrackingTouch progress=\"+mProgressView.getProgress());\n\n            if (mPlayer != null) {\n                int progress = mProgressView.getProgress();\n                int seekPosition = (int)(progress * 1.0f / MAX_PROGRESS * mDuration);\n                mPlayer.seekTo(seekPosition);\n\n                mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);\n            }\n        }\n    };\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        if (mPlayer != null) {\n            mPlayer.pause();\n            mControlBtn.setBackgroundResource(R.mipmap.paused_state);\n        }\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        mHandler.removeMessages(MSG_UPDATE_PROGRESS);\n        doReleasePlayer();\n    }\n\n    private IPlayer.OnPreparedListener mPreparedListener = new IPlayer.OnPreparedListener() {\n        @Override\n        public void onPrepared(IPlayer mp) {\n            doPlayVideo();\n        }\n    };\n\n    private IPlayer.OnErrorListener mErrorListener = new IPlayer.OnErrorListener() {\n        @Override\n        public void onError(IPlayer mp, int what, String msg) {\n            Toast.makeText(PlayerActivity.this, \"Play Error\", Toast.LENGTH_SHORT).show();\n        }\n    };\n\n    private IPlayer.OnVideoSizeChangedListener mVideoSizeChangeListener = new IPlayer.OnVideoSizeChangedListener() {\n\n        @Override\n        public void onVideoSizeChanged(IPlayer mp, int width, int height, int rotationDegree, float pixelRatio, float darRatio) {\n\n            LogUtils.d(\"PlayerActivity onVideoSizeChanged width=\"+width+\", height=\"+height + \", mDarRatio = \" + darRatio);\n            mVideoWidth = width;\n            mVideoHeight = height;\n            mPixelRatio = pixelRatio;\n            mDarRatio = darRatio;\n            if (mPlayerType != 1 || Math.abs(mDarRatio) < 0.001f) {\n                mSurfaceHeight = (int) (mSurfaceWidth * mVideoHeight * 1.0f / mVideoWidth);\n            } else {\n                mSurfaceHeight = (int) (mSurfaceWidth * 1.0f / mDarRatio);\n            }\n            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mSurfaceWidth, mSurfaceHeight);\n            params.gravity = Gravity.CENTER;\n            mVideoView.setLayoutParams(params);\n        }\n    };\n\n    private IPlayer.OnLocalProxyCacheListener mOnLocalProxyCacheListener = new IPlayer.OnLocalProxyCacheListener() {\n        @Override\n        public void onCacheReady(IPlayer mp, String proxyUrl) {\n            Uri uri = Uri.parse(proxyUrl);\n            try {\n                mPlayer.setDataSource(PlayerActivity.this, uri);\n            } catch (IOException e) {\n                e.printStackTrace();\n                return;\n            }\n            mPlayer.setSurface(mSurface);\n            mPlayer.setOnPreparedListener(mPreparedListener);\n            mPlayer.setOnVideoSizeChangedListener(mVideoSizeChangeListener);\n            mPlayer.setOnErrorListener(mErrorListener);\n            mPlayer.prepareAsync();\n            mPlayTipView.setVisibility(View.VISIBLE);\n        }\n\n        @Override\n        public void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize) {\n            mPercent = percent;\n            mCacheSize = cachedSize;\n            mPlayTipView.setText(\"边下边播： \" + Utility.getSize(cachedSize));\n        }\n\n        @Override\n        public void onCacheSpeedChanged(IPlayer mp, float speed) {\n        }\n\n        @Override\n        public void onCacheForbidden(IPlayer mp, String url) {\n            LogUtils.w(\"onCacheForbidden url = \" + url);\n        }\n\n        @Override\n        public void onCacheFinished(IPlayer mp) {\n            mPercent = 100;\n        }\n    };\n\n    private static final int MSG_UPDATE_PROGRESS = 1;\n    private static final int MAX_PROGRESS = 1000;\n\n    private Handler mHandler = new Handler() {\n\n        @Override\n        public void handleMessage(Message msg) {\n            if (msg.what == MSG_UPDATE_PROGRESS) {\n                updateProgressView();\n            }\n        }\n    };\n\n    @Override\n    public void onClick(View view) {\n        LogUtils.e(\"click event\");\n        if(view == mControlBtn) {\n            if (!mPlayer.isPlaying()) {\n                mPlayer.start();\n                mControlBtn.setBackgroundResource(R.mipmap.played_state);\n            } else {\n                mPlayer.pause();\n                mControlBtn.setBackgroundResource(R.mipmap.paused_state);\n            }\n        }\n    }\n\n    private void doPlayVideo() {\n        if (mPlayer != null) {\n            mTimeView.setVisibility(View.VISIBLE);\n            mPlayer.start();\n            mControlBtn.setBackgroundResource(R.mipmap.played_state);\n            mDuration = mPlayer.getDuration();\n            LogUtils.d(\"total duration =\"+mDuration +\", timeString=\"+ Utility.getVideoTimeString(mDuration));\n            mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);\n        }\n    }\n\n    private void updateProgressView() {\n        if (mPlayer != null) {\n            long currentPosition = mPlayer.getCurrentPosition();\n            mTimeView.setText(Utility.getVideoTimeString(currentPosition) + \" / \" + Utility.getVideoTimeString(mDuration));\n            mProgressView.setProgress((int)(1000 *  currentPosition * 1.0f / mDuration));\n            int cacheProgress = (int)(mPercent * 1.0f / 100 * 1000);\n            mProgressView.setSecondaryProgress(cacheProgress);\n        }\n        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, 1000);\n    }\n\n    private void doReleasePlayer() {\n        if (mPlayer != null) {\n            mPlayer.stop();\n            mPlayer.release();\n            mPlayer = null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/android/media/VideoListAdapter.java",
    "content": "package com.android.media;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.model.VideoTaskItem;\nimport com.media.cache.model.VideoTaskState;\n\npublic class VideoListAdapter extends ArrayAdapter<VideoTaskItem> {\n\n    private Context mContext;\n\n    public VideoListAdapter(Context context, int resource, VideoTaskItem[] items) {\n        super(context, resource, items);\n        mContext = context;\n    }\n\n    @NonNull\n    @Override\n    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {\n        View view = LayoutInflater.from(getContext()).inflate(R.layout.download_item, null);\n        VideoTaskItem item = getItem(position);\n        TextView urlTextView = (TextView) view.findViewById(R.id.url_text);\n        urlTextView.setText(item.getUrl());\n        TextView stateTextView = (TextView) view.findViewById(R.id.status_txt);\n        TextView playBtn = (TextView) view.findViewById(R.id.download_play_btn);\n        playBtn.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                Intent intent = new Intent(mContext, DownloadPlayActivity.class);\n                intent.putExtra(\"proxy_url\", item.getProxyUrl());\n                intent.putExtra(\"origin_url\", item.getUrl());\n                mContext.startActivity(intent);\n            }\n        });\n        setStateText(stateTextView, playBtn, item);\n        TextView infoTextView = (TextView) view.findViewById(R.id.download_txt);\n        setDownloadInfoText(infoTextView, item);\n\n        return view;\n    }\n\n    private void setStateText(TextView stateView, TextView playBtn, VideoTaskItem item) {\n        switch (item.getTaskState()) {\n            case VideoTaskState.PENDING:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"等待中\");\n                break;\n            case VideoTaskState.PREPARE:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"准备好\");\n                break;\n            case VideoTaskState.START:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"开始下载\");\n                break;\n            case VideoTaskState.DOWNLOADING:\n            case VideoTaskState.PROXYREADY:\n                if (item.getProxyReady()) {\n                    stateView.setText(\"下载中...(可播放)\");\n                    playBtn.setVisibility(View.VISIBLE);\n                } else {\n                    stateView.setText(\"下载中...\");\n                }\n                break;\n            case VideoTaskState.PAUSE:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"下载暂停\");\n                break;\n            case VideoTaskState.SUCCESS:\n                playBtn.setVisibility(View.VISIBLE);\n                stateView.setText(\"下载完成, 总大小=\" + item.getDownloadSizeString());\n                break;\n            case VideoTaskState.ERROR:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"下载错误\");\n                break;\n            default:\n                playBtn.setVisibility(View.INVISIBLE);\n                stateView.setText(\"未下载\");\n                break;\n\n        }\n    }\n\n    private void setDownloadInfoText(TextView infoView, VideoTaskItem item) {\n        switch (item.getTaskState()) {\n            case VideoTaskState.DOWNLOADING:\n                infoView.setText(\"进度:\" + item.getPercentString() + \", 速度:\" + item.getSpeedString() +\", 已下载:\" + item.getDownloadSizeString());\n                break;\n            case VideoTaskState.SUCCESS:\n                infoView.setText(\"进度:\" + item.getPercentString());\n                break;\n            case VideoTaskState.PAUSE:\n                infoView.setText(\"进度:\" + item.getPercentString());\n                break;\n            default:\n                break;\n        }\n    }\n\n    public void notifyChanged(VideoTaskItem[] items, VideoTaskItem item) {\n        for (int index = 0; index < getCount(); index++) {\n            if (getItem(index).equals(item)) {\n                items[index] = item;\n                notifyDataSetChanged();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/border.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape>\n            <stroke android:width=\"0.5dp\" android:color=\"@color/colorPrimaryDark\" /><!--边框颜色-->\n            <solid android:color=\"#f8f8f8\" /><!--填充色-->\n            <corners android:radius=\"4dp\" />\n        </shape>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/layout/activity_download_feature.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:background=\"@color/white\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_marginLeft=\"30dp\"\n        android:layout_marginTop=\"50dp\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/download_settings_btn\"\n            android:textSize=\"18sp\"\n            android:textColor=\"@color/white\"\n            android:text=\"下载设置\"\n            android:layout_width=\"180dp\"\n            android:layout_height=\"wrap_content\" />\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/download_orcode\"\n            android:text=\"二维码扫描\"\n            android:textSize=\"18sp\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"180dp\"\n            android:layout_marginTop=\"20dp\"\n            android:layout_height=\"wrap_content\" />\n\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/download_normal_btn\"\n            android:text=\"普通下载\"\n            android:textSize=\"18sp\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"180dp\"\n            android:layout_marginTop=\"20dp\"\n            android:layout_height=\"wrap_content\" />\n\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/list_download_btn\"\n            android:text=\"查看已下载任务\"\n            android:textSize=\"18sp\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"180dp\"\n            android:layout_marginTop=\"20dp\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_download_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <TextView\n        android:id=\"@+id/file_path\"\n        android:textSize=\"16sp\"\n        android:text=\"下载文件路径\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <LinearLayout\n        android:id=\"@+id/top_layout\"\n        android:layout_below=\"@+id/file_path\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"60dp\">\n        <Button\n            android:id=\"@+id/pause_task_btn\"\n            android:text=\"暂停所有下载任务\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n        \n        <Button\n            android:id=\"@+id/clear_cache_btn\"\n            android:text=\"清理下载数据\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n    </LinearLayout>\n    \n    <ListView\n        android:id=\"@+id/download_listview\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:layout_below=\"@+id/top_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_download_play.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:id=\"@+id/download_player_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\" >\n\n        <TextureView\n            android:id=\"@+id/download_video_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n        <SeekBar\n            android:id=\"@+id/download_progress_view\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginBottom=\"10dp\"\n            android:max=\"1000\"\n            android:secondaryProgress=\"0\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"10dp\" />\n\n        <TextView\n            android:id=\"@+id/download_time_view\"\n            android:layout_gravity=\"center\"\n            android:text=\"XXXX / XXXX\"\n            android:textColor=\"#ff0000\"\n            android:textSize=\"20sp\"\n            android:visibility=\"gone\"\n            android:layout_marginBottom=\"5dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <ImageButton\n            android:id=\"@+id/download_control_btn\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_marginTop=\"15dp\"\n            android:clickable=\"true\"\n            android:layout_gravity=\"center_horizontal\"\n            android:background=\"#ffffffff\"\n            android:src=\"@mipmap/paused_state\" />\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_download_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:background=\"@color/white\"\n    android:scrollbars=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:orientation=\"vertical\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n        <TextView\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginLeft=\"10dp\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"18sp\"\n            android:padding=\"5dp\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:text=\"存储位置\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/store_loc_txt\"\n            android:layout_marginLeft=\"10dp\"\n            android:layout_marginTop=\"5dp\"\n            android:textColor=\"@color/black\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <LinearLayout\n            android:orientation=\"horizontal\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" >\n\n            <TextView\n                android:layout_marginTop=\"5dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:textColor=\"@color/white\"\n                android:textSize=\"18sp\"\n                android:padding=\"5dp\"\n                android:background=\"@color/colorPrimaryDark\"\n                android:text=\"缓存文件大小\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n            <TextView\n                android:id=\"@+id/clear_download_cache\"\n                android:layout_marginTop=\"5dp\"\n                android:padding=\"5dp\"\n                android:textSize=\"18sp\"\n                android:text=\"清理缓存\"\n                android:textColor=\"@color/white\"\n                android:background=\"@color/colorPrimaryDark\"\n                android:layout_marginLeft=\"20dp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </LinearLayout>\n\n\n        <TextView\n            android:layout_marginLeft=\"10dp\"\n            android:layout_marginTop=\"5dp\"\n            android:id=\"@+id/store_size\"\n            android:textColor=\"@color/black\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/open_file_txt\"\n            android:layout_marginTop=\"5dp\"\n            android:layout_marginLeft=\"10dp\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"18sp\"\n            android:padding=\"5dp\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:text=\"打开存储目录\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:layout_marginTop=\"10dp\"\n            android:background=\"@color/black\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"2px\" />\n\n        <TextView\n            android:textSize=\"18sp\"\n            android:padding=\"5dp\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:textColor=\"@color/white\"\n            android:text=\"最大并发数\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <RadioGroup\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" >\n\n            <RadioButton\n                android:id=\"@+id/btn1\"\n                android:textColor=\"@color/black\"\n                android:text=\"1\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn2\"\n                android:textColor=\"@color/black\"\n                android:text=\"2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn3\"\n                android:checked=\"true\"\n                android:textColor=\"@color/black\"\n                android:text=\"3\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn4\"\n                android:textColor=\"@color/black\"\n                android:text=\"4\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn5\"\n                android:textColor=\"@color/black\"\n                android:text=\"5\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </RadioGroup>\n\n        <TextView\n            android:layout_marginTop=\"10dp\"\n            android:background=\"@color/black\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"2px\" />\n\n        <TextView\n            android:textSize=\"18sp\"\n            android:padding=\"5dp\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:textColor=\"@color/white\"\n            android:text=\"是否忽略证书\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n        <RadioGroup\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" >\n            <RadioButton\n                android:id=\"@+id/btn11\"\n                android:textColor=\"@color/black\"\n                android:checked=\"true\"\n                android:text=\"是\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn12\"\n                android:textColor=\"@color/black\"\n                android:text=\"否\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </RadioGroup>\n\n        <TextView\n            android:layout_marginTop=\"10dp\"\n            android:background=\"@color/black\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"2px\" />\n\n        <TextView\n            android:textSize=\"18sp\"\n            android:padding=\"5dp\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:textColor=\"@color/white\"\n            android:text=\"移动网络下是否暂停下载\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n        <RadioGroup\n            android:layout_marginLeft=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" >\n            <RadioButton\n                android:id=\"@+id/btn13\"\n                android:textColor=\"@color/black\"\n                android:checked=\"true\"\n                android:text=\"是\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:id=\"@+id/btn14\"\n                android:textColor=\"@color/black\"\n                android:text=\"否\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </RadioGroup>\n    </LinearLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_marginLeft=\"30dp\"\n        android:layout_marginTop=\"50dp\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/download_btn\"\n            android:textSize=\"20sp\"\n            android:textColor=\"@color/white\"\n            android:text=\"下载模块\"\n            android:layout_width=\"180dp\"\n            android:layout_height=\"wrap_content\" />\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:id=\"@+id/play_btn\"\n            android:text=\"播放模块\"\n            android:textSize=\"20sp\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"180dp\"\n            android:layout_marginTop=\"20dp\"\n            android:layout_height=\"wrap_content\" />\n        <Button\n            android:background=\"@color/colorPrimaryDark\"\n            android:text=\"视频检索\"\n            android:textSize=\"20sp\"\n            android:textColor=\"@color/white\"\n            android:layout_marginTop=\"20dp\"\n            android:id=\"@+id/scan_btn\"\n            android:layout_width=\"180dp\"\n            android:layout_height=\"wrap_content\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_orcode.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    \n    <EditText\n        android:padding=\"10dp\"\n        android:gravity=\"top\"\n        android:hint=\"请输入下载的url\"\n        android:id=\"@+id/download_url_text\"\n        android:layout_margin=\"10dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"100dp\" />\n\n    <Button\n        android:id=\"@+id/download_single_btn\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_width=\"150dp\"\n        android:textSize=\"16sp\"\n        android:text=\"点击下载\"\n        android:layout_height=\"wrap_content\" />\n\n    <Button\n        android:id=\"@+id/orcode_scanner_btn\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_width=\"150dp\"\n        android:textSize=\"16sp\"\n        android:text=\"扫码下载\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_play_func.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:focusable=\"true\"\n    android:focusableInTouchMode=\"true\"\n    android:orientation=\"vertical\"\n    android:paddingBottom=\"@dimen/activity_vertical_margin\"\n    android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n    android:paddingRight=\"@dimen/activity_horizontal_margin\"\n    android:paddingTop=\"@dimen/activity_vertical_margin\"\n    android:background=\"@color/white\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <EditText\n        android:padding=\"10dp\"\n        android:id=\"@+id/video_url_view\"\n        android:textSize=\"16sp\"\n        android:hint=\"请输入视频url\"\n        android:text=\"https://tv.youkutv.cc/2020/01/15/SZpLQDUmJZKF9O0D/playlist.m3u8\"\n        android:background=\"@drawable/border\"\n        android:textColor=\"@color/black\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"100dp\"\n        android:gravity=\"top\" />\n\n    <LinearLayout\n        android:layout_marginTop=\"10dp\"\n        android:layout_gravity=\"center_vertical\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\" >\n\n        <Button\n            android:id=\"@+id/play_btn\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:paddingLeft=\"15dp\"\n            android:paddingRight=\"15dp\"\n            android:textColor=\"@color/white\"\n            android:text=\"开始播放\" />\n\n        <RadioGroup\n            android:layout_marginTop=\"10dp\"\n            android:id=\"@+id/play_btn_group\"\n            android:layout_marginLeft=\"10dp\"\n            android:layout_marginRight=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\" >\n\n            <RadioButton\n                android:id=\"@+id/ijkplayer_btn\"\n                android:textSize=\"15sp\"\n                android:text=\"IjkPlayer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <RadioButton\n                android:layout_marginLeft=\"10dp\"\n                android:id=\"@+id/exoplayer_btn\"\n                android:textSize=\"15sp\"\n                android:text=\"ExoPlayer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n        </RadioGroup>\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_marginTop=\"10dp\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" >\n\n        <CheckBox\n            android:id=\"@+id/local_proxy_box\"\n            android:text=\"边下边播\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"15sp\"\n            android:layout_marginRight=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/clear_cache_view\"\n            android:layout_marginLeft=\"20dp\"\n            android:text=\"清理缓存\"\n            android:background=\"@color/colorPrimaryDark\"\n            android:textSize=\"16sp\"\n            android:paddingLeft=\"15dp\"\n            android:paddingRight=\"15dp\"\n            android:paddingTop=\"5dp\"\n            android:paddingBottom=\"5dp\"\n            android:textColor=\"@color/white\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/cache_size_view\"\n            android:text=\"缓存大小\"\n            android:textSize=\"16sp\"\n            android:textColor=\"@color/black\"\n            android:layout_marginLeft=\"20dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_marginTop=\"10dp\"\n        android:id=\"@+id/video_layout\"\n        android:orientation=\"vertical\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n\n        <TextView\n            android:id=\"@+id/cached_location_view\"\n            android:layout_marginLeft=\"5dp\"\n            android:text=\"存储位置\"\n            android:textColor=\"@color/black\"\n            android:layout_marginTop=\"5dp\"\n            android:textSize=\"15sp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n        <ListView\n            android:id=\"@+id/video_list\"\n            android:divider=\"@color/colorPrimaryDark\"\n            android:dividerHeight=\"2px\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\">\n        </ListView>\n\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:id=\"@+id/player_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n        \n        <LinearLayout\n            android:background=\"#000000\"\n            android:orientation=\"vertical\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n            <TextureView\n                android:id=\"@+id/video_view\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"300dp\" />\n        </LinearLayout>\n\n        <SeekBar\n            android:id=\"@+id/video_progress_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"10dp\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginBottom=\"10dp\"\n            android:max=\"1000\"\n            android:secondaryProgress=\"0\" />\n\n    </LinearLayout>\n    \n    <LinearLayout\n        android:layout_marginLeft=\"10dp\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" >\n        <ImageButton\n            android:id=\"@+id/video_control_btn\"\n            android:layout_width=\"50dp\"\n            android:layout_height=\"50dp\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"15dp\"\n            android:clickable=\"true\"\n            android:background=\"@mipmap/paused_state\" />\n\n        <TextView\n            android:id=\"@+id/video_time_view\"\n            android:layout_marginLeft=\"30dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginBottom=\"5dp\"\n            android:visibility=\"invisible\"\n            android:text=\"XXXX / XXXX\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"15sp\" />\n    </LinearLayout>\n    \n    <LinearLayout\n        android:layout_marginLeft=\"10dp\"\n        android:orientation=\"vertical\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" >\n        <TextView\n            android:textColor=\"@color/colorPrimaryDark\"\n            android:textSize=\"18sp\"\n            android:visibility=\"invisible\"\n            android:id=\"@+id/play_tip_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_scanner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/download_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    \n    <TextView\n        android:layout_margin=\"5dp\"\n        android:id=\"@+id/url_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n    \n    <LinearLayout\n        android:orientation=\"horizontal\"\n        android:layout_margin=\"5dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" >\n        <TextView\n            android:id=\"@+id/status_txt\"\n            android:text=\"未下载\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            android:id=\"@+id/download_play_btn\"\n            android:textSize=\"25sp\"\n            android:text=\"点击播放\"\n            android:visibility=\"invisible\"\n            android:textColor=\"#ff0000\"\n            android:layout_marginLeft=\"20dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n\n    <TextView\n        android:id=\"@+id/download_txt\"\n        android:layout_margin=\"5dp\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/video_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <TextView\n        android:id=\"@+id/video_name\"\n        android:layout_width=\"wrap_content\"\n        android:textSize=\"20sp\"\n        android:layout_marginTop=\"5dp\"\n        android:layout_marginBottom=\"5dp\"\n        android:layout_marginStart=\"10dp\"\n        android:textColor=\"#000000\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n    <color name=\"white\">#ffffff</color>\n    <color name=\"black\">#000000</color>\n    <color name=\"green\">#31d0ff</color>\n    <color name=\"gray\">#dcdcdc</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MediaSDK</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "base/.gitignore",
    "content": "/build\n/base.iml\n"
  },
  {
    "path": "base/base.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":base\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":base\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_7\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 27 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.collection:collection:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.annotation:annotation:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.fragment:fragment:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.viewpager:viewpager:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.loader:loader:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.activity:activity:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.customview:customview:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.core:core:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.savedstate:savedstate:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.interpolator:interpolator:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-runtime:2.0.0@aar\" level=\"project\" />\n  </component>\n</module>"
  },
  {
    "path": "base/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 27\n    buildToolsVersion \"27.0.2\"\n\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 27\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n}\n"
  },
  {
    "path": "base/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "base/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.netlib\" />\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/MediaSDKReceiver.java",
    "content": "package com.android.baselib;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.android.baselib.utils.NetworkUtils;\n\npublic class MediaSDKReceiver extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n        LogUtils.w(\"Current Network:\" + NetworkUtils.getNetworkType(context));\n    }\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/NetworkCallbackImpl.java",
    "content": "package com.android.baselib;\n\nimport android.annotation.SuppressLint;\nimport android.net.ConnectivityManager;\nimport android.net.Network;\nimport android.net.NetworkCapabilities;\n\nimport androidx.annotation.NonNull;\n\nimport com.android.baselib.utils.LogUtils;\n\n@SuppressLint(\"NewApi\")\npublic class NetworkCallbackImpl extends ConnectivityManager.NetworkCallback {\n\n    private NetworkListener mListener;\n\n    public NetworkCallbackImpl(NetworkListener listener) {\n        mListener = listener;\n    }\n\n    @Override\n    public void onAvailable(@NonNull Network network) {\n        mListener.onAvailable();\n    }\n\n    @Override\n    public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {\n        LogUtils.e(\"onCapabilitiesChanged: \"+ networkCapabilities);\n        if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {\n            if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {\n                mListener.onWifiConnected();\n            } else {\n                mListener.onMobileConnected();\n            }\n        }\n    }\n\n    @Override\n    public void onLost(@NonNull Network network) {\n    }\n\n    @Override\n    public void onUnavailable() {\n        mListener.onUnConnected();\n    }\n}"
  },
  {
    "path": "base/src/main/java/com/android/baselib/NetworkListener.java",
    "content": "package com.android.baselib;\n\npublic interface NetworkListener {\n\n    void onAvailable();\n\n    void onWifiConnected();\n\n    void onMobileConnected();\n\n    void onNetworkType();\n\n    void onUnConnected();\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/WeakHandler.java",
    "content": "package com.android.baselib;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\n\nimport java.lang.ref.WeakReference;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * Memory safer implementation of android.os.Handler\n * <p/>\n * Original implementation of Handlers always keeps hard reference to handler in queue of execution.\n * If you create anonymous handler and post delayed message into it, it will keep all parent class\n * for that time in memory even if it could be cleaned.\n * <p/>\n * This implementation is trickier, it will keep WeakReferences to runnables and messages,\n * and GC could collect them once WeakHandler instance is not referenced any more\n * <p/>\n *\n * @see Handler\n *\n * Created by Dmytro Voronkevych on 17/06/2014.\n */\n@SuppressWarnings(\"unused\")\npublic class WeakHandler {\n    private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory\n    private final ExecHandler mExec;\n    private Lock mLock = new ReentrantLock();\n    @SuppressWarnings(\"ConstantConditions\")\n    @VisibleForTesting\n    final ChainedRef mRunnables = new ChainedRef(mLock, null);\n\n    /**\n     * Default constructor associates this handler with the {@link Looper} for the\n     * current thread.\n     *\n     * If this thread does not have a looper, this handler won't be able to receive messages\n     * so an exception is thrown.\n     */\n    public WeakHandler() {\n        mCallback = null;\n        mExec = new ExecHandler();\n    }\n\n    /**\n     * Constructor associates this handler with the {@link Looper} for the\n     * current thread and takes a callback interface in which you can handle\n     * messages.\n     *\n     * If this thread does not have a looper, this handler won't be able to receive messages\n     * so an exception is thrown.\n     *\n     * @param callback The callback interface in which to handle messages, or null.\n     */\n    public WeakHandler(@Nullable Handler.Callback callback) {\n        mCallback = callback; // Hard referencing body\n        mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler\n    }\n\n    /**\n     * Use the provided {@link Looper} instead of the default one.\n     *\n     * @param looper The looper, must not be null.\n     */\n    public WeakHandler(@NonNull Looper looper) {\n        mCallback = null;\n        mExec = new ExecHandler(looper);\n    }\n\n    /**\n     * Use the provided {@link Looper} instead of the default one and take a callback\n     * interface in which to handle messages.\n     *\n     * @param looper The looper, must not be null.\n     * @param callback The callback interface in which to handle messages, or null.\n     */\n    public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) {\n        mCallback = callback;\n        mExec = new ExecHandler(looper, new WeakReference<>(callback));\n    }\n\n    /**\n     * Causes the Runnable r to be added to the message queue.\n     * The runnable will be run on the thread to which this handler is\n     * attached.\n     *\n     * @param r The Runnable that will be executed.\n     *\n     * @return Returns true if the Runnable was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean post(@NonNull Runnable r) {\n        return mExec.post(wrapRunnable(r));\n    }\n\n    /**\n     * Causes the Runnable r to be added to the message queue, to be run\n     * at a specific time given by <var>uptimeMillis</var>.\n     * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>\n     * The runnable will be run on the thread to which this handler is attached.\n     *\n     * @param r The Runnable that will be executed.\n     * @param uptimeMillis The absolute time at which the callback should run,\n     *         using the {@link android.os.SystemClock#uptimeMillis} time-base.\n     *\n     * @return Returns true if the Runnable was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.  Note that a\n     *         result of true does not mean the Runnable will be processed -- if\n     *         the looper is quit before the delivery time of the message\n     *         occurs then the message will be dropped.\n     */\n    public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {\n        return mExec.postAtTime(wrapRunnable(r), uptimeMillis);\n    }\n\n    /**\n     * Causes the Runnable r to be added to the message queue, to be run\n     * at a specific time given by <var>uptimeMillis</var>.\n     * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>\n     * The runnable will be run on the thread to which this handler is attached.\n     *\n     * @param r The Runnable that will be executed.\n     * @param uptimeMillis The absolute time at which the callback should run,\n     *         using the {@link android.os.SystemClock#uptimeMillis} time-base.\n     *\n     * @return Returns true if the Runnable was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.  Note that a\n     *         result of true does not mean the Runnable will be processed -- if\n     *         the looper is quit before the delivery time of the message\n     *         occurs then the message will be dropped.\n     *\n     * @see android.os.SystemClock#uptimeMillis\n     */\n    public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {\n        return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis);\n    }\n\n    /**\n     * Causes the Runnable r to be added to the message queue, to be run\n     * after the specified amount of time elapses.\n     * The runnable will be run on the thread to which this handler\n     * is attached.\n     *\n     * @param r The Runnable that will be executed.\n     * @param delayMillis The delay (in milliseconds) until the Runnable\n     *        will be executed.\n     *\n     * @return Returns true if the Runnable was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.  Note that a\n     *         result of true does not mean the Runnable will be processed --\n     *         if the looper is quit before the delivery time of the message\n     *         occurs then the message will be dropped.\n     */\n    public final boolean postDelayed(Runnable r, long delayMillis) {\n        return mExec.postDelayed(wrapRunnable(r), delayMillis);\n    }\n\n    /**\n     * Posts a message to an object that implements Runnable.\n     * Causes the Runnable r to executed on the next iteration through the\n     * message queue. The runnable will be run on the thread to which this\n     * handler is attached.\n     * <b>This method is only for use in very special circumstances -- it\n     * can easily starve the message queue, cause ordering problems, or have\n     * other unexpected side-effects.</b>\n     *\n     * @param r The Runnable that will be executed.\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean postAtFrontOfQueue(Runnable r) {\n        return mExec.postAtFrontOfQueue(wrapRunnable(r));\n    }\n\n    /**\n     * Remove any pending posts of Runnable r that are in the message queue.\n     */\n    public final void removeCallbacks(Runnable r) {\n        final WeakRunnable runnable = mRunnables.remove(r);\n        if (runnable != null) {\n            mExec.removeCallbacks(runnable);\n        }\n    }\n\n    /**\n     * Remove any pending posts of Runnable <var>r</var> with Object\n     * <var>token</var> that are in the message queue.  If <var>token</var> is null,\n     * all callbacks will be removed.\n     */\n    public final void removeCallbacks(Runnable r, Object token) {\n        final WeakRunnable runnable = mRunnables.remove(r);\n        if (runnable != null) {\n            mExec.removeCallbacks(runnable, token);\n        }\n    }\n\n    /**\n     * Pushes a message onto the end of the message queue after all pending messages\n     * before the current time. It will be received in callback,\n     * in the thread attached to this handler.\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean sendMessage(Message msg) {\n        return mExec.sendMessage(msg);\n    }\n\n    /**\n     * Sends a Message containing only the what value.\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean sendEmptyMessage(int what) {\n        return mExec.sendEmptyMessage(what);\n    }\n\n    /**\n     * Sends a Message containing only the what value, to be delivered\n     * after the specified amount of time elapses.\n     * @see #sendMessageDelayed(Message, long)\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {\n        return mExec.sendEmptyMessageDelayed(what, delayMillis);\n    }\n\n    /**\n     * Sends a Message containing only the what value, to be delivered\n     * at a specific time.\n     * @see #sendMessageAtTime(Message, long)\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {\n        return mExec.sendEmptyMessageAtTime(what, uptimeMillis);\n    }\n\n    /**\n     * Enqueue a message into the message queue after all pending messages\n     * before (current time + delayMillis). You will receive it in\n     * callback, in the thread attached to this handler.\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.  Note that a\n     *         result of true does not mean the message will be processed -- if\n     *         the looper is quit before the delivery time of the message\n     *         occurs then the message will be dropped.\n     */\n    public final boolean sendMessageDelayed(Message msg, long delayMillis) {\n        return mExec.sendMessageDelayed(msg, delayMillis);\n    }\n\n    /**\n     * Enqueue a message into the message queue after all pending messages\n     * before the absolute time (in milliseconds) <var>uptimeMillis</var>.\n     * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>\n     * You will receive it in callback, in the thread attached\n     * to this handler.\n     *\n     * @param uptimeMillis The absolute time at which the message should be\n     *         delivered, using the\n     *         {@link android.os.SystemClock#uptimeMillis} time-base.\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.  Note that a\n     *         result of true does not mean the message will be processed -- if\n     *         the looper is quit before the delivery time of the message\n     *         occurs then the message will be dropped.\n     */\n    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {\n        return mExec.sendMessageAtTime(msg, uptimeMillis);\n    }\n\n    /**\n     * Enqueue a message at the front of the message queue, to be processed on\n     * the next iteration of the message loop.  You will receive it in\n     * callback, in the thread attached to this handler.\n     * <b>This method is only for use in very special circumstances -- it\n     * can easily starve the message queue, cause ordering problems, or have\n     * other unexpected side-effects.</b>\n     *\n     * @return Returns true if the message was successfully placed in to the\n     *         message queue.  Returns false on failure, usually because the\n     *         looper processing the message queue is exiting.\n     */\n    public final boolean sendMessageAtFrontOfQueue(Message msg) {\n        return mExec.sendMessageAtFrontOfQueue(msg);\n    }\n\n    /**\n     * Remove any pending posts of messages with code 'what' that are in the\n     * message queue.\n     */\n    public final void removeMessages(int what) {\n        mExec.removeMessages(what);\n    }\n\n    /**\n     * Remove any pending posts of messages with code 'what' and whose obj is\n     * 'object' that are in the message queue.  If <var>object</var> is null,\n     * all messages will be removed.\n     */\n    public final void removeMessages(int what, Object object) {\n        mExec.removeMessages(what, object);\n    }\n\n    /**\n     * Remove any pending posts of callbacks and sent messages whose\n     * <var>obj</var> is <var>token</var>.  If <var>token</var> is null,\n     * all callbacks and messages will be removed.\n     */\n    public final void removeCallbacksAndMessages(Object token) {\n        mExec.removeCallbacksAndMessages(token);\n    }\n\n    /**\n     * Check if there are any pending posts of messages with code 'what' in\n     * the message queue.\n     */\n    public final boolean hasMessages(int what) {\n        return mExec.hasMessages(what);\n    }\n\n    /**\n     * Check if there are any pending posts of messages with code 'what' and\n     * whose obj is 'object' in the message queue.\n     */\n    public final boolean hasMessages(int what, Object object) {\n        return mExec.hasMessages(what, object);\n    }\n\n    public final Looper getLooper() {\n        return mExec.getLooper();\n    }\n\n    private WeakRunnable wrapRunnable(@NonNull Runnable r) {\n        //noinspection ConstantConditions\n        if (r == null) {\n            throw new NullPointerException(\"Runnable can't be null\");\n        }\n        final ChainedRef hardRef = new ChainedRef(mLock, r);\n        mRunnables.insertAfter(hardRef);\n        return hardRef.wrapper;\n    }\n\n    private static class ExecHandler extends Handler {\n        private final WeakReference<Callback> mCallback;\n\n        ExecHandler() {\n            mCallback = null;\n        }\n\n        ExecHandler(WeakReference<Callback> callback) {\n            mCallback = callback;\n        }\n\n        ExecHandler(Looper looper) {\n            super(looper);\n            mCallback = null;\n        }\n\n        ExecHandler(Looper looper, WeakReference<Callback> callback) {\n            super(looper);\n            mCallback = callback;\n        }\n\n        @Override\n        public void handleMessage(@NonNull Message msg) {\n            if (mCallback == null) {\n                return;\n            }\n            final Callback callback = mCallback.get();\n            if (callback == null) { // Already disposed\n                return;\n            }\n            callback.handleMessage(msg);\n        }\n    }\n\n    static class WeakRunnable implements Runnable {\n        private final WeakReference<Runnable> mDelegate;\n        private final WeakReference<ChainedRef> mReference;\n\n        WeakRunnable(WeakReference<Runnable> delegate, WeakReference<ChainedRef> reference) {\n            mDelegate = delegate;\n            mReference = reference;\n        }\n\n        @Override\n        public void run() {\n            final Runnable delegate = mDelegate.get();\n            final ChainedRef reference = mReference.get();\n            if (reference != null) {\n                reference.remove();\n            }\n            if (delegate != null) {\n                delegate.run();\n            }\n        }\n    }\n\n    static class ChainedRef {\n        @Nullable\n        ChainedRef next;\n        @Nullable\n        ChainedRef prev;\n        @NonNull\n        final Runnable runnable;\n        @NonNull\n        final WeakRunnable wrapper;\n\n        @NonNull\n        Lock lock;\n\n        public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) {\n            this.runnable = r;\n            this.lock = lock;\n            this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this));\n        }\n\n        public WeakRunnable remove() {\n            lock.lock();\n            try {\n                if (prev != null) {\n                    prev.next = next;\n                }\n                if (next != null) {\n                    next.prev = prev;\n                }\n                prev = null;\n                next = null;\n            } finally {\n                lock.unlock();\n            }\n            return wrapper;\n        }\n\n        public void insertAfter(@NonNull ChainedRef candidate) {\n            lock.lock();\n            try {\n                if (this.next != null) {\n                    this.next.prev = candidate;\n                }\n\n                candidate.next = this.next;\n                this.next = candidate;\n                candidate.prev = this;\n            } finally {\n                lock.unlock();\n            }\n        }\n\n        @Nullable\n        public WeakRunnable remove(Runnable obj) {\n            lock.lock();\n            try {\n                ChainedRef curr = this.next; // Skipping head\n                while (curr != null) {\n                    if (curr.runnable == obj) { // We do comparison exactly how Handler does inside\n                        return curr.remove();\n                    }\n                    curr = curr.next;\n                }\n            } finally {\n                lock.unlock();\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/utils/LogUtils.java",
    "content": "package com.android.baselib.utils;\n\nimport android.util.Log;\n\npublic class LogUtils {\n    private static final String TAG = \"MediaSDK\";\n    private static final int ERROR_LEVEL = 5;\n    private static final int WARN_LEVEL = 4;\n    private static final int DEBUG_LEVEL = 3;\n    private static final int INFO_LEVEL = 2;\n    private static int sLogLevel = 2;\n\n    public static void e(String msg) {\n        if (sLogLevel <= ERROR_LEVEL) {\n            Log.e(TAG, msg);\n        }\n    }\n\n    public static void i(String msg) {\n        if (sLogLevel <= INFO_LEVEL) {\n            Log.i(TAG, msg);\n        }\n    }\n\n    public static void d(String msg) {\n        if (sLogLevel <= DEBUG_LEVEL) {\n            Log.d(TAG, msg);\n        }\n    }\n\n    public static void w(String msg) {\n        if (sLogLevel <= WARN_LEVEL) {\n            Log.w(TAG, msg);\n        }\n    }\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/utils/NetworkUtils.java",
    "content": "package com.android.baselib.utils;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.telephony.TelephonyManager;\n\npublic class NetworkUtils {\n\n    public static final int CONNECTION_TYPE_NULL = 0;\n    public static final int CONNECTION_TYPE_WIFI = 2;\n    public static final int CONNECTION_TYPE_4G = 3;\n    public static final int CONNECTION_TYPE_3G = 4;\n    public static final int CONNECTION_TYPE_2G = 5;\n\n    @SuppressWarnings({\"MissingPermission\"})\n    public static boolean isNetworkConnected(Context context) {\n        if (context != null) {\n            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n            NetworkInfo networkInfo = manager.getActiveNetworkInfo();\n            if (networkInfo != null) {\n                return networkInfo.isAvailable();\n            }\n        }\n        return false;\n    }\n\n    @SuppressWarnings({\"MissingPermission\"})\n    public static boolean isWifiConnected(Context context) {\n        if (context != null) {\n            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n            NetworkInfo networkInfo = manager.getActiveNetworkInfo();\n            if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {\n                return networkInfo.isAvailable();\n            }\n        }\n        return false;\n    }\n\n    @SuppressWarnings({\"MissingPermission\"})\n    public static boolean isMobileConnected(Context context) {\n        if (context != null) {\n            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n            NetworkInfo networkInfo = manager.getActiveNetworkInfo();\n            if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {\n                return networkInfo.isAvailable();\n            }\n        }\n        return false;\n    }\n\n    @SuppressWarnings({\"MissingPermission\"})\n    public static int getAPNType(Context context) {\n        int netType = CONNECTION_TYPE_NULL;\n        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n        NetworkInfo networkInfo = manager.getActiveNetworkInfo();\n        if (networkInfo == null) {\n            return CONNECTION_TYPE_NULL;\n        }\n        int nType = networkInfo.getType();\n        if (nType == ConnectivityManager.TYPE_WIFI) {\n            netType = CONNECTION_TYPE_WIFI;\n        } else if (nType == ConnectivityManager.TYPE_MOBILE) {\n            int nSubType = networkInfo.getSubtype();\n            TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);\n            if (nSubType == TelephonyManager.NETWORK_TYPE_LTE\n                    && !telephonyManager.isNetworkRoaming()) {\n                netType = CONNECTION_TYPE_4G;\n            } else if (nSubType == TelephonyManager.NETWORK_TYPE_UMTS\n                    || nSubType == TelephonyManager.NETWORK_TYPE_HSDPA\n                    || nSubType == TelephonyManager.NETWORK_TYPE_EVDO_0\n                    && !telephonyManager.isNetworkRoaming()) {\n                netType = CONNECTION_TYPE_3G;\n            } else if (nSubType == TelephonyManager.NETWORK_TYPE_GPRS\n                    || nSubType == TelephonyManager.NETWORK_TYPE_EDGE\n                    || nSubType == TelephonyManager.NETWORK_TYPE_CDMA\n                    && !telephonyManager.isNetworkRoaming()) {\n                netType = CONNECTION_TYPE_2G;\n            } else {\n                netType = CONNECTION_TYPE_2G;\n            }\n        }\n        return netType;\n    }\n\n    public static String getNetworkType(Context context) {\n        int apnType = getAPNType(context);\n        if (apnType == CONNECTION_TYPE_NULL) {\n            return \"NULL\";\n        } else if (apnType == CONNECTION_TYPE_WIFI) {\n            return \"WIFI\";\n        } else if (apnType == CONNECTION_TYPE_4G) {\n            return \"4G\";\n        } else if (apnType == CONNECTION_TYPE_3G) {\n            return \"3G\";\n        } else if (apnType == CONNECTION_TYPE_2G) {\n            return \"2G\";\n        }\n        return \"NULL\";\n    }\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/utils/ScreenUtils.java",
    "content": "package com.android.baselib.utils;\n\nimport android.content.Context;\nimport android.util.DisplayMetrics;\n\npublic class ScreenUtils {\n\n    public static int getScreenWidth(Context context) {\n        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();\n        return displayMetrics.widthPixels;\n    }\n\n    public static int getScreenHeight(Context context) {\n        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();\n        return displayMetrics.heightPixels;\n    }\n}\n"
  },
  {
    "path": "base/src/main/java/com/android/baselib/utils/Utility.java",
    "content": "package com.android.baselib.utils;\n\nimport java.text.DecimalFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\npublic class Utility {\n\n    public static String getSize(long size) {\n        StringBuffer sb = new StringBuffer();\n        DecimalFormat format = new DecimalFormat(\"###.00\");\n        if (size >= 1024 * 1024 * 1024) {\n            double i = (size / (1024.0 * 1024.0 * 1024.0));\n            sb.append(format.format(i)).append(\"GB\");\n        } else if (size >= 1024 * 1024) {\n            double i = (size / (1024.0 * 1024.0));\n            sb.append(format.format(i)).append(\"MB\");\n        } else if (size >= 1024) {\n            double i = (size / (1024.0));\n            sb.append(format.format(i)).append(\"KB\");\n        } else if (size < 1024) {\n            if (size <= 0) {\n                sb.append(\"0B\");\n            } else {\n                sb.append((int) size).append(\"B\");\n            }\n        }\n        return sb.toString();\n    }\n\n    public static String getPercent(float percent) {\n        DecimalFormat format = new DecimalFormat(\"###.00\");\n        return format.format(percent) + \"%\";\n    }\n\n    public static String getTime(long time) {\n        SimpleDateFormat formatter = new SimpleDateFormat(\"HH:mm:ss:SSS\");\n        String formatStr = formatter.format(new Date(time));\n        return formatStr;\n    }\n\n    public static String getVideoTimeString(long duration) {\n        duration /= 1000;\n        String DateTimes = null;\n        long hours = (duration % ( 60 * 60 * 24)) / (60 * 60);\n        long minutes = (duration % ( 60 * 60)) /60;\n        long seconds = duration % 60;\n\n        DateTimes=String.format(\"%02d:\", hours)+ String.format(\"%02d:\", minutes) + String.format(\"%02d\", seconds);\n        String.format(\"%2d:\", hours);\n        return DateTimes;\n    }\n\n    public static String getTimeShow(long timeMillis) {\n\n        String timeShow;\n        float time = timeMillis / 1000f / 60f;\n        if (time < 1) {\n            time = timeMillis / 1000f;\n            timeShow = time + \" s \";\n        } else {\n            timeShow = time + \" min \";\n        }\n        return timeShow;\n    }\n}\n"
  },
  {
    "path": "base/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">netlib</string>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        jcenter()\n        google()\n        \n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.5.3'\n        \n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        jcenter()\n        google()\n\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "constants.gradle",
    "content": "// Copyright (C) 2017 The Android Open Source Project\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.\nproject.ext {\n    // ExoPlayer version and version code.\n    releaseVersion = '2.11.1'\n    releaseVersionCode = 2011001\n    minSdkVersion = 16\n    appTargetSdkVersion = 29\n    targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved\n    compileSdkVersion = 29\n    dexmakerVersion = '2.21.0'\n    guavaVersion = '23.5-android'\n    mockitoVersion = '2.25.0'\n    robolectricVersion = '4.3'\n    autoValueVersion = '1.6'\n    autoServiceVersion = '1.0-rc4'\n    checkerframeworkVersion = '2.5.0'\n    jsr305Version = '3.0.2'\n    kotlinAnnotationsVersion = '1.3.31'\n    androidxAnnotationVersion = '1.1.0'\n    androidxAppCompatVersion = '1.1.0'\n    androidxCollectionVersion = '1.1.0'\n    androidxMediaVersion = '1.0.1'\n    androidxTestCoreVersion = '1.2.0'\n    androidxTestJUnitVersion = '1.1.1'\n    androidxTestRunnerVersion = '1.2.0'\n    androidxTestRulesVersion = '1.2.0'\n    truthVersion = '0.44'\n    modulePrefix = ':'\n    if (gradle.ext.has('exoplayerModulePrefix')) {\n        modulePrefix += gradle.ext.exoplayerModulePrefix\n    }\n}\n"
  },
  {
    "path": "exoplayer/.gitignore",
    "content": "/build\n/exoplayer.iml\n"
  },
  {
    "path": "exoplayer/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply from: '../constants.gradle'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion \"29.0.3\"\n\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion\n    implementation project(path: ':base')\n    compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion\n    compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion\n}\n"
  },
  {
    "path": "exoplayer/exoplayer.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":exoplayer\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":exoplayer\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_8\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 29 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"Gradle: com.google.code.findbugs:jsr305:3.0.2@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: org.checkerframework:checker-qual:2.5.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: org.checkerframework:checker-compat-qual:2.5.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: org.jetbrains.kotlin:kotlin-annotations-jvm:1.3.31@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.annotation:annotation:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"module\" module-name=\"base\" />\n  </component>\n</module>"
  },
  {
    "path": "exoplayer/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "exoplayer/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.exoplayerlib\" />\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/AudioBecomingNoisyManager.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.media.AudioManager;\nimport android.os.Handler;\n\n/* package */ final class AudioBecomingNoisyManager {\n\n  private final Context context;\n  private final AudioBecomingNoisyReceiver receiver;\n  private boolean receiverRegistered;\n\n  public interface EventListener {\n    void onAudioBecomingNoisy();\n  }\n\n  public AudioBecomingNoisyManager(Context context, Handler eventHandler, EventListener listener) {\n    this.context = context.getApplicationContext();\n    this.receiver = new AudioBecomingNoisyReceiver(eventHandler, listener);\n  }\n\n  /**\n   * Enables the {@link AudioBecomingNoisyManager} which calls {@link\n   * EventListener#onAudioBecomingNoisy()} upon receiving an intent of {@link\n   * AudioManager#ACTION_AUDIO_BECOMING_NOISY}.\n   *\n   * @param enabled True if the listener should be notified when audio is becoming noisy.\n   */\n  public void setEnabled(boolean enabled) {\n    if (enabled && !receiverRegistered) {\n      context.registerReceiver(\n          receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));\n      receiverRegistered = true;\n    } else if (!enabled && receiverRegistered) {\n      context.unregisterReceiver(receiver);\n      receiverRegistered = false;\n    }\n  }\n\n  private final class AudioBecomingNoisyReceiver extends BroadcastReceiver implements Runnable {\n    private final EventListener listener;\n    private final Handler eventHandler;\n\n    public AudioBecomingNoisyReceiver(Handler eventHandler, EventListener listener) {\n      this.eventHandler = eventHandler;\n      this.listener = listener;\n    }\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n      if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {\n        eventHandler.post(this);\n      }\n    }\n\n    @Override\n    public void run() {\n      if (receiverRegistered) {\n        listener.onAudioBecomingNoisy();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.content.Context;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport android.os.Handler;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** Manages requesting and responding to changes in audio focus. */\n/* package */ final class AudioFocusManager {\n\n  /** Interface to allow AudioFocusManager to give commands to a player. */\n  public interface PlayerControl {\n    /**\n     * Called when the volume multiplier on the player should be changed.\n     *\n     * @param volumeMultiplier The new volume multiplier.\n     */\n    void setVolumeMultiplier(float volumeMultiplier);\n\n    /**\n     * Called when a command must be executed on the player.\n     *\n     * @param playerCommand The command that must be executed.\n     */\n    void executePlayerCommand(@PlayerCommand int playerCommand);\n  }\n\n  /**\n   * Player commands. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link\n   * #PLAYER_COMMAND_WAIT_FOR_CALLBACK} or {@link #PLAYER_COMMAND_PLAY_WHEN_READY}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    PLAYER_COMMAND_DO_NOT_PLAY,\n    PLAYER_COMMAND_WAIT_FOR_CALLBACK,\n    PLAYER_COMMAND_PLAY_WHEN_READY,\n  })\n  public @interface PlayerCommand {}\n  /** Do not play. */\n  public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1;\n  /** Do not play now. Wait for callback to play. */\n  public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0;\n  /** Play freely. */\n  public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1;\n\n  /** Audio focus state. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    AUDIO_FOCUS_STATE_LOST_FOCUS,\n    AUDIO_FOCUS_STATE_NO_FOCUS,\n    AUDIO_FOCUS_STATE_HAVE_FOCUS,\n    AUDIO_FOCUS_STATE_LOSS_TRANSIENT,\n    AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK\n  })\n  private @interface AudioFocusState {}\n  /** No audio focus was held, but has been lost by another app taking it permanently. */\n  private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1;\n  /** No audio focus is currently being held. */\n  private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0;\n  /** The requested audio focus is currently held. */\n  private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1;\n  /** Audio focus has been temporarily lost. */\n  private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2;\n  /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */\n  private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3;\n\n  private static final String TAG = \"AudioFocusManager\";\n\n  private static final float VOLUME_MULTIPLIER_DUCK = 0.2f;\n  private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f;\n\n  private final AudioManager audioManager;\n  private final AudioFocusListener focusListener;\n  private final PlayerControl playerControl;\n  @Nullable private AudioAttributes audioAttributes;\n\n  @AudioFocusState private int audioFocusState;\n  @C.AudioFocusGain private int focusGain;\n  private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT;\n\n  private @MonotonicNonNull AudioFocusRequest audioFocusRequest;\n  private boolean rebuildAudioFocusRequest;\n\n  /**\n   * Constructs an AudioFocusManager to automatically handle audio focus for a player.\n   *\n   * @param context The current context.\n   * @param eventHandler A {@link Handler} to for the thread on which the player is used.\n   * @param playerControl A {@link PlayerControl} to handle commands from this instance.\n   */\n  public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) {\n    this.audioManager =\n        (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);\n    this.playerControl = playerControl;\n    this.focusListener = new AudioFocusListener(eventHandler);\n    this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;\n  }\n\n  /** Gets the current player volume multiplier. */\n  public float getVolumeMultiplier() {\n    return volumeMultiplier;\n  }\n\n  /**\n   * Sets audio attributes that should be used to manage audio focus.\n   *\n   * @param audioAttributes The audio attributes or {@code null} if audio focus should not be\n   *     managed automatically.\n   * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}.\n   * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}.\n   * @return A {@link PlayerCommand} to execute on the player.\n   */\n  @PlayerCommand\n  public int setAudioAttributes(\n      @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) {\n    if (!Util.areEqual(this.audioAttributes, audioAttributes)) {\n      this.audioAttributes = audioAttributes;\n      focusGain = convertAudioAttributesToFocusGain(audioAttributes);\n\n      Assertions.checkArgument(\n          focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE,\n          \"Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME.\");\n      if (playWhenReady\n          && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) {\n        return requestAudioFocus();\n      }\n    }\n\n    return playerState == Player.STATE_IDLE\n        ? handleIdle(playWhenReady)\n        : handlePrepare(playWhenReady);\n  }\n\n  /**\n   * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}.\n   *\n   * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}.\n   * @return A {@link PlayerCommand} to execute on the player.\n   */\n  @PlayerCommand\n  public int handlePrepare(boolean playWhenReady) {\n    return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY;\n  }\n\n  /**\n   * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}.\n   *\n   * @param playWhenReady The desired value of playWhenReady.\n   * @param playerState The current state of the player.\n   * @return A {@link PlayerCommand} to execute on the player.\n   */\n  @PlayerCommand\n  public int handleSetPlayWhenReady(boolean playWhenReady, int playerState) {\n    if (!playWhenReady) {\n      abandonAudioFocus();\n      return PLAYER_COMMAND_DO_NOT_PLAY;\n    }\n\n    return playerState == Player.STATE_IDLE ? handleIdle(playWhenReady) : requestAudioFocus();\n  }\n\n  /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */\n  public void handleStop() {\n    abandonAudioFocus(/* forceAbandon= */ true);\n  }\n\n  // Internal methods.\n\n  @VisibleForTesting\n  /* package */ AudioManager.OnAudioFocusChangeListener getFocusListener() {\n    return focusListener;\n  }\n\n  @PlayerCommand\n  private int handleIdle(boolean playWhenReady) {\n    return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY;\n  }\n\n  @PlayerCommand\n  private int requestAudioFocus() {\n    int focusRequestResult;\n\n    if (focusGain == C.AUDIOFOCUS_NONE) {\n      if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) {\n        abandonAudioFocus(/* forceAbandon= */ true);\n      }\n      return PLAYER_COMMAND_PLAY_WHEN_READY;\n    }\n\n    if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {\n      if (Util.SDK_INT >= 26) {\n        focusRequestResult = requestAudioFocusV26();\n      } else {\n        focusRequestResult = requestAudioFocusDefault();\n      }\n      audioFocusState =\n          focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED\n              ? AUDIO_FOCUS_STATE_HAVE_FOCUS\n              : AUDIO_FOCUS_STATE_NO_FOCUS;\n    }\n\n    if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {\n      return PLAYER_COMMAND_DO_NOT_PLAY;\n    }\n\n    return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT\n        ? PLAYER_COMMAND_WAIT_FOR_CALLBACK\n        : PLAYER_COMMAND_PLAY_WHEN_READY;\n  }\n\n  private void abandonAudioFocus() {\n    abandonAudioFocus(/* forceAbandon= */ false);\n  }\n\n  private void abandonAudioFocus(boolean forceAbandon) {\n    if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {\n      return;\n    }\n\n    if (focusGain != C.AUDIOFOCUS_GAIN\n        || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS\n        || forceAbandon) {\n      if (Util.SDK_INT >= 26) {\n        abandonAudioFocusV26();\n      } else {\n        abandonAudioFocusDefault();\n      }\n      audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;\n    }\n  }\n\n  private int requestAudioFocusDefault() {\n    return audioManager.requestAudioFocus(\n        focusListener,\n        Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage),\n        focusGain);\n  }\n\n  @RequiresApi(26)\n  private int requestAudioFocusV26() {\n    if (audioFocusRequest == null || rebuildAudioFocusRequest) {\n      AudioFocusRequest.Builder builder =\n          audioFocusRequest == null\n              ? new AudioFocusRequest.Builder(focusGain)\n              : new AudioFocusRequest.Builder(audioFocusRequest);\n\n      boolean willPauseWhenDucked = willPauseWhenDucked();\n      audioFocusRequest =\n          builder\n              .setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21())\n              .setWillPauseWhenDucked(willPauseWhenDucked)\n              .setOnAudioFocusChangeListener(focusListener)\n              .build();\n\n      rebuildAudioFocusRequest = false;\n    }\n    return audioManager.requestAudioFocus(audioFocusRequest);\n  }\n\n  private void abandonAudioFocusDefault() {\n    audioManager.abandonAudioFocus(focusListener);\n  }\n\n  @RequiresApi(26)\n  private void abandonAudioFocusV26() {\n    if (audioFocusRequest != null) {\n      audioManager.abandonAudioFocusRequest(audioFocusRequest);\n    }\n  }\n\n  private boolean willPauseWhenDucked() {\n    return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH;\n  }\n\n  /**\n   * Converts {@link AudioAttributes} to one of the audio focus request.\n   *\n   * <p>This follows the class Javadoc of {@link AudioFocusRequest}.\n   *\n   * @param audioAttributes The audio attributes associated with this focus request.\n   * @return The type of audio focus gain that should be requested.\n   */\n  @C.AudioFocusGain\n  private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) {\n\n    if (audioAttributes == null) {\n      // Don't handle audio focus. It may be either video only contents or developers\n      // want to have more finer grained control. (e.g. adding audio focus listener)\n      return C.AUDIOFOCUS_NONE;\n    }\n\n    switch (audioAttributes.usage) {\n        // USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times\n        // during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that.\n        // Don't request audio focus here.\n      case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:\n        return C.AUDIOFOCUS_NONE;\n\n        // Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music\n        // playback, for a game or a video player'\n      case C.USAGE_GAME:\n      case C.USAGE_MEDIA:\n        return C.AUDIOFOCUS_GAIN;\n\n        // Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent\n        // multiple media playback happen at the same time.\n      case C.USAGE_UNKNOWN:\n        Log.w(\n            TAG,\n            \"Specify a proper usage in the audio attributes for audio focus\"\n                + \" handling. Using AUDIOFOCUS_GAIN by default.\");\n        return C.AUDIOFOCUS_GAIN;\n\n        // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or\n        // during a VoIP call'\n      case C.USAGE_ALARM:\n      case C.USAGE_VOICE_COMMUNICATION:\n        return C.AUDIOFOCUS_GAIN_TRANSIENT;\n\n        // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing\n        // driving directions or notifications'\n      case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:\n      case C.USAGE_ASSISTANCE_SONIFICATION:\n      case C.USAGE_NOTIFICATION:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:\n      case C.USAGE_NOTIFICATION_EVENT:\n      case C.USAGE_NOTIFICATION_RINGTONE:\n        return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;\n\n        // Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing\n        // audio recording or speech recognition'.\n        // Assistant is considered as both recording and notifying developer\n      case C.USAGE_ASSISTANT:\n        if (Util.SDK_INT >= 19) {\n          return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;\n        } else {\n          return C.AUDIOFOCUS_GAIN_TRANSIENT;\n        }\n\n        // Special usages:\n      case C.USAGE_ASSISTANCE_ACCESSIBILITY:\n        if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) {\n          // Voice shouldn't be interrupted by other playback.\n          return C.AUDIOFOCUS_GAIN_TRANSIENT;\n        }\n        return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;\n      default:\n        Log.w(TAG, \"Unidentified audio usage: \" + audioAttributes.usage);\n        return C.AUDIOFOCUS_NONE;\n    }\n  }\n\n  private void handleAudioFocusChange(int focusChange) {\n    // Convert the platform focus change to internal state.\n    switch (focusChange) {\n      case AudioManager.AUDIOFOCUS_LOSS:\n        audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS;\n        break;\n      case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n        audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;\n        break;\n      case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n        if (willPauseWhenDucked()) {\n          audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;\n        } else {\n          audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK;\n        }\n        break;\n      case AudioManager.AUDIOFOCUS_GAIN:\n        audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS;\n        break;\n      default:\n        Log.w(TAG, \"Unknown focus change type: \" + focusChange);\n        // Early return.\n        return;\n    }\n\n    // Handle the internal state (change).\n    switch (audioFocusState) {\n      case AUDIO_FOCUS_STATE_NO_FOCUS:\n        // Focus was not requested; nothing to do.\n        break;\n      case AUDIO_FOCUS_STATE_LOST_FOCUS:\n        playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);\n        abandonAudioFocus(/* forceAbandon= */ true);\n        break;\n      case AUDIO_FOCUS_STATE_LOSS_TRANSIENT:\n        playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);\n        break;\n      case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK:\n        // Volume will be adjusted by the code below.\n        break;\n      case AUDIO_FOCUS_STATE_HAVE_FOCUS:\n        playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY);\n        break;\n      default:\n        throw new IllegalStateException(\"Unknown audio focus state: \" + audioFocusState);\n    }\n\n    float volumeMultiplier =\n        (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)\n            ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK\n            : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;\n    if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) {\n      AudioFocusManager.this.volumeMultiplier = volumeMultiplier;\n      playerControl.setVolumeMultiplier(volumeMultiplier);\n    }\n  }\n\n  // Internal audio focus listener.\n\n  private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {\n    private final Handler eventHandler;\n\n    public AudioFocusListener(Handler eventHandler) {\n      this.eventHandler = eventHandler;\n    }\n\n    @Override\n    public void onAudioFocusChange(int focusChange) {\n      eventHandler.post(() -> handleAudioFocusChange(focusChange));\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/BasePlayer.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Abstract base {@link Player} which implements common implementation independent methods. */\npublic abstract class BasePlayer implements Player {\n\n  protected final Timeline.Window window;\n\n  public BasePlayer() {\n    window = new Timeline.Window();\n  }\n\n  @Override\n  public final boolean isPlaying() {\n    return getPlaybackState() == Player.STATE_READY\n        && getPlayWhenReady()\n        && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;\n  }\n\n  @Override\n  public final void seekToDefaultPosition() {\n    seekToDefaultPosition(getCurrentWindowIndex());\n  }\n\n  @Override\n  public final void seekToDefaultPosition(int windowIndex) {\n    seekTo(windowIndex, /* positionMs= */ C.TIME_UNSET);\n  }\n\n  @Override\n  public final void seekTo(long positionMs) {\n    seekTo(getCurrentWindowIndex(), positionMs);\n  }\n\n  @Override\n  public final boolean hasPrevious() {\n    return getPreviousWindowIndex() != C.INDEX_UNSET;\n  }\n\n  @Override\n  public final void previous() {\n    int previousWindowIndex = getPreviousWindowIndex();\n    if (previousWindowIndex != C.INDEX_UNSET) {\n      seekToDefaultPosition(previousWindowIndex);\n    }\n  }\n\n  @Override\n  public final boolean hasNext() {\n    return getNextWindowIndex() != C.INDEX_UNSET;\n  }\n\n  @Override\n  public final void next() {\n    int nextWindowIndex = getNextWindowIndex();\n    if (nextWindowIndex != C.INDEX_UNSET) {\n      seekToDefaultPosition(nextWindowIndex);\n    }\n  }\n\n  @Override\n  public final void stop() {\n    stop(/* reset= */ false);\n  }\n\n  @Override\n  public final int getNextWindowIndex() {\n    Timeline timeline = getCurrentTimeline();\n    return timeline.isEmpty()\n        ? C.INDEX_UNSET\n        : timeline.getNextWindowIndex(\n            getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());\n  }\n\n  @Override\n  public final int getPreviousWindowIndex() {\n    Timeline timeline = getCurrentTimeline();\n    return timeline.isEmpty()\n        ? C.INDEX_UNSET\n        : timeline.getPreviousWindowIndex(\n            getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());\n  }\n\n  @Override\n  @Nullable\n  public final Object getCurrentTag() {\n    Timeline timeline = getCurrentTimeline();\n    return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).tag;\n  }\n\n  @Override\n  @Nullable\n  public final Object getCurrentManifest() {\n    Timeline timeline = getCurrentTimeline();\n    return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).manifest;\n  }\n\n  @Override\n  public final int getBufferedPercentage() {\n    long position = getBufferedPosition();\n    long duration = getDuration();\n    return position == C.TIME_UNSET || duration == C.TIME_UNSET\n        ? 0\n        : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);\n  }\n\n  @Override\n  public final boolean isCurrentWindowDynamic() {\n    Timeline timeline = getCurrentTimeline();\n    return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;\n  }\n\n  @Override\n  public final boolean isCurrentWindowLive() {\n    Timeline timeline = getCurrentTimeline();\n    return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive;\n  }\n\n  @Override\n  public final boolean isCurrentWindowSeekable() {\n    Timeline timeline = getCurrentTimeline();\n    return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;\n  }\n\n  @Override\n  public final long getContentDuration() {\n    Timeline timeline = getCurrentTimeline();\n    return timeline.isEmpty()\n        ? C.TIME_UNSET\n        : timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();\n  }\n\n  @RepeatMode\n  private int getRepeatModeForNavigation() {\n    @RepeatMode int repeatMode = getRepeatMode();\n    return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;\n  }\n\n  /** Holds a listener reference. */\n  protected static final class ListenerHolder {\n\n    /**\n     * The listener on which {link #invoke} will execute {@link ListenerInvocation listener\n     * invocations}.\n     */\n    public final EventListener listener;\n\n    private boolean released;\n\n    public ListenerHolder(EventListener listener) {\n      this.listener = listener;\n    }\n\n    /** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */\n    public void release() {\n      released = true;\n    }\n\n    /**\n     * Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link\n     * #release} has been called on this instance.\n     */\n    public void invoke(ListenerInvocation listenerInvocation) {\n      if (!released) {\n        listenerInvocation.invokeListener(listener);\n      }\n    }\n\n    @Override\n    public boolean equals(@Nullable Object other) {\n      if (this == other) {\n        return true;\n      }\n      if (other == null || getClass() != other.getClass()) {\n        return false;\n      }\n      return listener.equals(((ListenerHolder) other).listener);\n    }\n\n    @Override\n    public int hashCode() {\n      return listener.hashCode();\n    }\n  }\n\n  /** Parameterized invocation of a {@link EventListener} method. */\n  protected interface ListenerInvocation {\n\n    /** Executes the invocation on the given {@link EventListener}. */\n    void invokeListener(EventListener listener);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/BaseRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * An abstract base class suitable for most {@link Renderer} implementations.\n */\npublic abstract class BaseRenderer implements Renderer, RendererCapabilities {\n\n  private final int trackType;\n  private final FormatHolder formatHolder;\n\n  private RendererConfiguration configuration;\n  private int index;\n  private int state;\n  private SampleStream stream;\n  private Format[] streamFormats;\n  private long streamOffsetUs;\n  private long readingPositionUs;\n  private boolean streamIsFinal;\n  private boolean throwRendererExceptionIsExecuting;\n\n  /**\n   * @param trackType The track type that the renderer handles. One of the {@link C}\n   * {@code TRACK_TYPE_*} constants.\n   */\n  public BaseRenderer(int trackType) {\n    this.trackType = trackType;\n    formatHolder = new FormatHolder();\n    readingPositionUs = C.TIME_END_OF_SOURCE;\n  }\n\n  @Override\n  public final int getTrackType() {\n    return trackType;\n  }\n\n  @Override\n  public final RendererCapabilities getCapabilities() {\n    return this;\n  }\n\n  @Override\n  public final void setIndex(int index) {\n    this.index = index;\n  }\n\n  @Override\n  @Nullable\n  public MediaClock getMediaClock() {\n    return null;\n  }\n\n  @Override\n  public final int getState() {\n    return state;\n  }\n\n  @Override\n  public final void enable(RendererConfiguration configuration, Format[] formats,\n      SampleStream stream, long positionUs, boolean joining, long offsetUs)\n      throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_DISABLED);\n    this.configuration = configuration;\n    state = STATE_ENABLED;\n    onEnabled(joining);\n    replaceStream(formats, stream, offsetUs);\n    onPositionReset(positionUs, joining);\n  }\n\n  @Override\n  public final void start() throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_ENABLED);\n    state = STATE_STARTED;\n    onStarted();\n  }\n\n  @Override\n  public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)\n      throws ExoPlaybackException {\n    Assertions.checkState(!streamIsFinal);\n    this.stream = stream;\n    readingPositionUs = offsetUs;\n    streamFormats = formats;\n    streamOffsetUs = offsetUs;\n    onStreamChanged(formats, offsetUs);\n  }\n\n  @Override\n  @Nullable\n  public final SampleStream getStream() {\n    return stream;\n  }\n\n  @Override\n  public final boolean hasReadStreamToEnd() {\n    return readingPositionUs == C.TIME_END_OF_SOURCE;\n  }\n\n  @Override\n  public final long getReadingPositionUs() {\n    return readingPositionUs;\n  }\n\n  @Override\n  public final void setCurrentStreamFinal() {\n    streamIsFinal = true;\n  }\n\n  @Override\n  public final boolean isCurrentStreamFinal() {\n    return streamIsFinal;\n  }\n\n  @Override\n  public final void maybeThrowStreamError() throws IOException {\n    stream.maybeThrowError();\n  }\n\n  @Override\n  public final void resetPosition(long positionUs) throws ExoPlaybackException {\n    streamIsFinal = false;\n    readingPositionUs = positionUs;\n    onPositionReset(positionUs, false);\n  }\n\n  @Override\n  public final void stop() throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_STARTED);\n    state = STATE_ENABLED;\n    onStopped();\n  }\n\n  @Override\n  public final void disable() {\n    Assertions.checkState(state == STATE_ENABLED);\n    formatHolder.clear();\n    state = STATE_DISABLED;\n    stream = null;\n    streamFormats = null;\n    streamIsFinal = false;\n    onDisabled();\n  }\n\n  @Override\n  public final void reset() {\n    Assertions.checkState(state == STATE_DISABLED);\n    formatHolder.clear();\n    onReset();\n  }\n\n  // RendererCapabilities implementation.\n\n  @Override\n  @AdaptiveSupport\n  public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n    return ADAPTIVE_NOT_SUPPORTED;\n  }\n\n  // PlayerMessage.Target implementation.\n\n  @Override\n  public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  // Methods to be overridden by subclasses.\n\n  /**\n   * Called when the renderer is enabled.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer's stream has changed. This occurs when the renderer is enabled after\n   * {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst\n   * the renderer is enabled or started.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param formats The enabled formats.\n   * @param offsetUs The offset that will be added to the timestamps of buffers read via\n   *     {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input\n   *     buffers have monotonically increasing timestamps.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the position is reset. This occurs when the renderer is enabled after\n   * {@link #onStreamChanged(Format[], long)} has been called, and also when a position\n   * discontinuity is encountered.\n   * <p>\n   * After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples\n   * starting from a key frame.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param positionUs The new playback position in microseconds.\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is started.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onStarted() throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is stopped.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onStopped() throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is disabled.\n   * <p>\n   * The default implementation is a no-op.\n   */\n  protected void onDisabled() {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is reset.\n   *\n   * <p>The default implementation is a no-op.\n   */\n  protected void onReset() {\n    // Do nothing.\n  }\n\n  // Methods to be called by subclasses.\n\n  /** Returns a clear {@link FormatHolder}. */\n  protected final FormatHolder getFormatHolder() {\n    formatHolder.clear();\n    return formatHolder;\n  }\n\n  /** Returns the formats of the currently enabled stream. */\n  protected final Format[] getStreamFormats() {\n    return streamFormats;\n  }\n\n  /**\n   * Returns the configuration set when the renderer was most recently enabled.\n   */\n  protected final RendererConfiguration getConfiguration() {\n    return configuration;\n  }\n\n  /** Returns a {@link DrmSession} ready for assignment, handling resource management. */\n  @Nullable\n  protected final <T extends ExoMediaCrypto> DrmSession<T> getUpdatedSourceDrmSession(\n      @Nullable Format oldFormat,\n      Format newFormat,\n      @Nullable DrmSessionManager<T> drmSessionManager,\n      @Nullable DrmSession<T> existingSourceSession)\n      throws ExoPlaybackException {\n    boolean drmInitDataChanged =\n        !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData);\n    if (!drmInitDataChanged) {\n      return existingSourceSession;\n    }\n    @Nullable DrmSession<T> newSourceDrmSession = null;\n    if (newFormat.drmInitData != null) {\n      if (drmSessionManager == null) {\n        throw createRendererException(\n            new IllegalStateException(\"Media requires a DrmSessionManager\"), newFormat);\n      }\n      newSourceDrmSession =\n          drmSessionManager.acquireSession(\n              Assertions.checkNotNull(Looper.myLooper()), newFormat.drmInitData);\n    }\n    if (existingSourceSession != null) {\n      existingSourceSession.release();\n    }\n    return newSourceDrmSession;\n  }\n\n  /**\n   * Returns the index of the renderer within the player.\n   */\n  protected final int getIndex() {\n    return index;\n  }\n\n  /**\n   * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for\n   * this renderer.\n   *\n   * @param cause The cause of the exception.\n   * @param format The current format used by the renderer. May be null.\n   */\n  protected final ExoPlaybackException createRendererException(\n      Exception cause, @Nullable Format format) {\n    @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED;\n    if (format != null && !throwRendererExceptionIsExecuting) {\n      // Prevent recursive re-entry from subclass supportsFormat implementations.\n      throwRendererExceptionIsExecuting = true;\n      try {\n        formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format));\n      } catch (ExoPlaybackException e) {\n        // Ignore, we are already failing.\n      } finally {\n        throwRendererExceptionIsExecuting = false;\n      }\n    }\n    return ExoPlaybackException.createForRenderer(cause, getIndex(), format, formatSupport);\n  }\n\n  /**\n   * Reads from the enabled upstream source. If the upstream source has been read to the end then\n   * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been\n   * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.\n   *\n   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.\n   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the\n   *     end of the stream. If the end of the stream has been reached, the {@link\n   *     C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.\n   * @param formatRequired Whether the caller requires that the format of the stream be read even if\n   *     it's not changing. A sample will never be read if set to true, however it is still possible\n   *     for the end of stream or nothing to be read.\n   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or\n   *     {@link C#RESULT_BUFFER_READ}.\n   */\n  protected final int readSource(\n      FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {\n    int result = stream.readData(formatHolder, buffer, formatRequired);\n    if (result == C.RESULT_BUFFER_READ) {\n      if (buffer.isEndOfStream()) {\n        readingPositionUs = C.TIME_END_OF_SOURCE;\n        return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;\n      }\n      buffer.timeUs += streamOffsetUs;\n      readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);\n    } else if (result == C.RESULT_FORMAT_READ) {\n      Format format = formatHolder.format;\n      if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {\n        format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs);\n        formatHolder.format = format;\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Attempts to skip to the keyframe before the specified position, or to the end of the stream if\n   * {@code positionUs} is beyond it.\n   *\n   * @param positionUs The position in microseconds.\n   * @return The number of samples that were skipped.\n   */\n  protected int skipSource(long positionUs) {\n    return stream.skipData(positionUs - streamOffsetUs);\n  }\n\n  /**\n   * Returns whether the upstream source is ready.\n   */\n  protected final boolean isSourceReady() {\n    return hasReadStreamToEnd() ? streamIsFinal : stream.isReady();\n  }\n\n  /**\n   * Returns whether {@code drmSessionManager} supports the specified {@code drmInitData}, or true\n   * if {@code drmInitData} is null.\n   *\n   * @param drmSessionManager The drm session manager.\n   * @param drmInitData {@link DrmInitData} of the format to check for support.\n   * @return Whether {@code drmSessionManager} supports the specified {@code drmInitData}, or\n   *     true if {@code drmInitData} is null.\n   */\n  protected static boolean supportsFormatDrm(@Nullable DrmSessionManager<?> drmSessionManager,\n      @Nullable DrmInitData drmInitData) {\n    if (drmInitData == null) {\n      // Content is unencrypted.\n      return true;\n    } else if (drmSessionManager == null) {\n      // Content is encrypted, but no drm session manager is available.\n      return false;\n    }\n    return drmSessionManager.canAcquireSession(drmInitData);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/C.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.media.AudioAttributes;\nimport android.media.AudioFormat;\nimport android.media.AudioManager;\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport android.view.Surface;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.audio.AuxEffectInfo;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;\nimport com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;\nimport com.google.android.exoplayer2.video.VideoFrameMetadataListener;\nimport com.google.android.exoplayer2.video.spherical.CameraMotionListener;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.UUID;\n\n/**\n * Defines constants used by the library.\n */\n@SuppressWarnings(\"InlinedApi\")\npublic final class C {\n\n  private C() {}\n\n  /**\n   * Special constant representing a time corresponding to the end of a source. Suitable for use in\n   * any time base.\n   */\n  public static final long TIME_END_OF_SOURCE = Long.MIN_VALUE;\n\n  /**\n   * Special constant representing an unset or unknown time or duration. Suitable for use in any\n   * time base.\n   */\n  public static final long TIME_UNSET = Long.MIN_VALUE + 1;\n\n  /**\n   * Represents an unset or unknown index.\n   */\n  public static final int INDEX_UNSET = -1;\n\n  /**\n   * Represents an unset or unknown position.\n   */\n  public static final int POSITION_UNSET = -1;\n\n  /**\n   * Represents an unset or unknown length.\n   */\n  public static final int LENGTH_UNSET = -1;\n\n  /** Represents an unset or unknown percentage. */\n  public static final int PERCENTAGE_UNSET = -1;\n\n  /** The number of milliseconds in one second. */\n  public static final long MILLIS_PER_SECOND = 1000L;\n\n  /** The number of microseconds in one second. */\n  public static final long MICROS_PER_SECOND = 1000000L;\n\n  /**\n   * The number of nanoseconds in one second.\n   */\n  public static final long NANOS_PER_SECOND = 1000000000L;\n\n  /** The number of bits per byte. */\n  public static final int BITS_PER_BYTE = 8;\n\n  /** The number of bytes per float. */\n  public static final int BYTES_PER_FLOAT = 4;\n\n  /**\n   * The name of the ASCII charset.\n   */\n  public static final String ASCII_NAME = \"US-ASCII\";\n  /**\n   * The name of the UTF-8 charset.\n   */\n  public static final String UTF8_NAME = \"UTF-8\";\n\n  /**\n   * The name of the UTF-16 charset.\n   */\n  public static final String UTF16_NAME = \"UTF-16\";\n\n  /** The name of the UTF-16 little-endian charset. */\n  public static final String UTF16LE_NAME = \"UTF-16LE\";\n\n  /**\n   * The name of the serif font family.\n   */\n  public static final String SERIF_NAME = \"serif\";\n\n  /**\n   * The name of the sans-serif font family.\n   */\n  public static final String SANS_SERIF_NAME = \"sans-serif\";\n\n  /**\n   * Crypto modes for a codec. One of {@link #CRYPTO_MODE_UNENCRYPTED}, {@link #CRYPTO_MODE_AES_CTR}\n   * or {@link #CRYPTO_MODE_AES_CBC}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})\n  public @interface CryptoMode {}\n  /**\n   * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED\n   */\n  public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;\n  /**\n   * @see MediaCodec#CRYPTO_MODE_AES_CTR\n   */\n  public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;\n  /**\n   * @see MediaCodec#CRYPTO_MODE_AES_CBC\n   */\n  public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;\n\n  /**\n   * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to\n   * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.\n   */\n  public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;\n\n  /**\n   * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},\n   * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link\n   * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link\n   * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link\n   * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},\n   * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    Format.NO_VALUE,\n    ENCODING_INVALID,\n    ENCODING_PCM_8BIT,\n    ENCODING_PCM_16BIT,\n    ENCODING_PCM_24BIT,\n    ENCODING_PCM_32BIT,\n    ENCODING_PCM_FLOAT,\n    ENCODING_PCM_MU_LAW,\n    ENCODING_PCM_A_LAW,\n    ENCODING_AC3,\n    ENCODING_E_AC3,\n    ENCODING_E_AC3_JOC,\n    ENCODING_AC4,\n    ENCODING_DTS,\n    ENCODING_DTS_HD,\n    ENCODING_DOLBY_TRUEHD,\n  })\n  public @interface Encoding {}\n\n  /**\n   * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},\n   * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link\n   * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link\n   * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    Format.NO_VALUE,\n    ENCODING_INVALID,\n    ENCODING_PCM_8BIT,\n    ENCODING_PCM_16BIT,\n    ENCODING_PCM_24BIT,\n    ENCODING_PCM_32BIT,\n    ENCODING_PCM_FLOAT,\n    ENCODING_PCM_MU_LAW,\n    ENCODING_PCM_A_LAW\n  })\n  public @interface PcmEncoding {}\n  /** @see AudioFormat#ENCODING_INVALID */\n  public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;\n  /** @see AudioFormat#ENCODING_PCM_8BIT */\n  public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;\n  /** @see AudioFormat#ENCODING_PCM_16BIT */\n  public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;\n  /** PCM encoding with 24 bits per sample. */\n  public static final int ENCODING_PCM_24BIT = 0x80000000;\n  /** PCM encoding with 32 bits per sample. */\n  public static final int ENCODING_PCM_32BIT = 0x40000000;\n  /** @see AudioFormat#ENCODING_PCM_FLOAT */\n  public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;\n  /** Audio encoding for mu-law. */\n  public static final int ENCODING_PCM_MU_LAW = 0x10000000;\n  /** Audio encoding for A-law. */\n  public static final int ENCODING_PCM_A_LAW = 0x20000000;\n  /** @see AudioFormat#ENCODING_AC3 */\n  public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;\n  /** @see AudioFormat#ENCODING_E_AC3 */\n  public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;\n  /** @see AudioFormat#ENCODING_E_AC3_JOC */\n  public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;\n  /** @see AudioFormat#ENCODING_AC4 */\n  public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;\n  /** @see AudioFormat#ENCODING_DTS */\n  public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;\n  /** @see AudioFormat#ENCODING_DTS_HD */\n  public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;\n  /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */\n  public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;\n\n  /**\n   * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link\n   * #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link\n   * #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link\n   * #STREAM_TYPE_USE_DEFAULT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    STREAM_TYPE_ALARM,\n    STREAM_TYPE_DTMF,\n    STREAM_TYPE_MUSIC,\n    STREAM_TYPE_NOTIFICATION,\n    STREAM_TYPE_RING,\n    STREAM_TYPE_SYSTEM,\n    STREAM_TYPE_VOICE_CALL,\n    STREAM_TYPE_USE_DEFAULT\n  })\n  public @interface StreamType {}\n  /**\n   * @see AudioManager#STREAM_ALARM\n   */\n  public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;\n  /**\n   * @see AudioManager#STREAM_DTMF\n   */\n  public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;\n  /**\n   * @see AudioManager#STREAM_MUSIC\n   */\n  public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;\n  /**\n   * @see AudioManager#STREAM_NOTIFICATION\n   */\n  public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;\n  /**\n   * @see AudioManager#STREAM_RING\n   */\n  public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;\n  /**\n   * @see AudioManager#STREAM_SYSTEM\n   */\n  public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;\n  /**\n   * @see AudioManager#STREAM_VOICE_CALL\n   */\n  public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;\n  /**\n   * @see AudioManager#USE_DEFAULT_STREAM_TYPE\n   */\n  public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;\n  /**\n   * The default stream type used by audio renderers.\n   */\n  public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;\n\n  /**\n   * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link\n   * #CONTENT_TYPE_MOVIE}, {@link #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link\n   * #CONTENT_TYPE_SPEECH} or {@link #CONTENT_TYPE_UNKNOWN}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    CONTENT_TYPE_MOVIE,\n    CONTENT_TYPE_MUSIC,\n    CONTENT_TYPE_SONIFICATION,\n    CONTENT_TYPE_SPEECH,\n    CONTENT_TYPE_UNKNOWN\n  })\n  public @interface AudioContentType {}\n  /**\n   * @see AudioAttributes#CONTENT_TYPE_MOVIE\n   */\n  public static final int CONTENT_TYPE_MOVIE = AudioAttributes.CONTENT_TYPE_MOVIE;\n  /**\n   * @see AudioAttributes#CONTENT_TYPE_MUSIC\n   */\n  public static final int CONTENT_TYPE_MUSIC = AudioAttributes.CONTENT_TYPE_MUSIC;\n  /**\n   * @see AudioAttributes#CONTENT_TYPE_SONIFICATION\n   */\n  public static final int CONTENT_TYPE_SONIFICATION =\n      AudioAttributes.CONTENT_TYPE_SONIFICATION;\n  /**\n   * @see AudioAttributes#CONTENT_TYPE_SPEECH\n   */\n  public static final int CONTENT_TYPE_SPEECH =\n      AudioAttributes.CONTENT_TYPE_SPEECH;\n  /**\n   * @see AudioAttributes#CONTENT_TYPE_UNKNOWN\n   */\n  public static final int CONTENT_TYPE_UNKNOWN =\n      AudioAttributes.CONTENT_TYPE_UNKNOWN;\n\n  /**\n   * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}. Possible flag value is\n   * {@link #FLAG_AUDIBILITY_ENFORCED}.\n   *\n   * <p>Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting\n   * the flag when tunneling is enabled via a track selector.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_AUDIBILITY_ENFORCED})\n  public @interface AudioFlags {}\n  /**\n   * @see AudioAttributes#FLAG_AUDIBILITY_ENFORCED\n   */\n  public static final int FLAG_AUDIBILITY_ENFORCED =\n      AudioAttributes.FLAG_AUDIBILITY_ENFORCED;\n\n  /**\n   * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link\n   * #USAGE_ALARM}, {@link #USAGE_ASSISTANCE_ACCESSIBILITY}, {@link\n   * #USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link #USAGE_ASSISTANCE_SONIFICATION}, {@link\n   * #USAGE_ASSISTANT}, {@link #USAGE_GAME}, {@link #USAGE_MEDIA}, {@link #USAGE_NOTIFICATION},\n   * {@link #USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link\n   * #USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link #USAGE_NOTIFICATION_COMMUNICATION_REQUEST},\n   * {@link #USAGE_NOTIFICATION_EVENT}, {@link #USAGE_NOTIFICATION_RINGTONE}, {@link\n   * #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or {@link\n   * #USAGE_VOICE_COMMUNICATION_SIGNALLING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    USAGE_ALARM,\n    USAGE_ASSISTANCE_ACCESSIBILITY,\n    USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,\n    USAGE_ASSISTANCE_SONIFICATION,\n    USAGE_ASSISTANT,\n    USAGE_GAME,\n    USAGE_MEDIA,\n    USAGE_NOTIFICATION,\n    USAGE_NOTIFICATION_COMMUNICATION_DELAYED,\n    USAGE_NOTIFICATION_COMMUNICATION_INSTANT,\n    USAGE_NOTIFICATION_COMMUNICATION_REQUEST,\n    USAGE_NOTIFICATION_EVENT,\n    USAGE_NOTIFICATION_RINGTONE,\n    USAGE_UNKNOWN,\n    USAGE_VOICE_COMMUNICATION,\n    USAGE_VOICE_COMMUNICATION_SIGNALLING\n  })\n  public @interface AudioUsage {}\n  /**\n   * @see AudioAttributes#USAGE_ALARM\n   */\n  public static final int USAGE_ALARM = AudioAttributes.USAGE_ALARM;\n  /** @see AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */\n  public static final int USAGE_ASSISTANCE_ACCESSIBILITY =\n      AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;\n  /**\n   * @see AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE\n   */\n  public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =\n      AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;\n  /**\n   * @see AudioAttributes#USAGE_ASSISTANCE_SONIFICATION\n   */\n  public static final int USAGE_ASSISTANCE_SONIFICATION =\n      AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;\n  /** @see AudioAttributes#USAGE_ASSISTANT */\n  public static final int USAGE_ASSISTANT = AudioAttributes.USAGE_ASSISTANT;\n  /**\n   * @see AudioAttributes#USAGE_GAME\n   */\n  public static final int USAGE_GAME = AudioAttributes.USAGE_GAME;\n  /**\n   * @see AudioAttributes#USAGE_MEDIA\n   */\n  public static final int USAGE_MEDIA = AudioAttributes.USAGE_MEDIA;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION\n   */\n  public static final int USAGE_NOTIFICATION = AudioAttributes.USAGE_NOTIFICATION;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =\n      AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =\n      AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =\n      AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION_EVENT\n   */\n  public static final int USAGE_NOTIFICATION_EVENT =\n      AudioAttributes.USAGE_NOTIFICATION_EVENT;\n  /**\n   * @see AudioAttributes#USAGE_NOTIFICATION_RINGTONE\n   */\n  public static final int USAGE_NOTIFICATION_RINGTONE =\n      AudioAttributes.USAGE_NOTIFICATION_RINGTONE;\n  /**\n   * @see AudioAttributes#USAGE_UNKNOWN\n   */\n  public static final int USAGE_UNKNOWN = AudioAttributes.USAGE_UNKNOWN;\n  /**\n   * @see AudioAttributes#USAGE_VOICE_COMMUNICATION\n   */\n  public static final int USAGE_VOICE_COMMUNICATION =\n      AudioAttributes.USAGE_VOICE_COMMUNICATION;\n  /**\n   * @see AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING\n   */\n  public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =\n      AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;\n\n  /**\n   * Capture policies for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link\n   * #ALLOW_CAPTURE_BY_ALL}, {@link #ALLOW_CAPTURE_BY_NONE} or {@link #ALLOW_CAPTURE_BY_SYSTEM}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({ALLOW_CAPTURE_BY_ALL, ALLOW_CAPTURE_BY_NONE, ALLOW_CAPTURE_BY_SYSTEM})\n  public @interface AudioAllowedCapturePolicy {}\n  /** See {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}. */\n  public static final int ALLOW_CAPTURE_BY_ALL = AudioAttributes.ALLOW_CAPTURE_BY_ALL;\n  /** See {@link AudioAttributes#ALLOW_CAPTURE_BY_NONE}. */\n  public static final int ALLOW_CAPTURE_BY_NONE = AudioAttributes.ALLOW_CAPTURE_BY_NONE;\n  /** See {@link AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. */\n  public static final int ALLOW_CAPTURE_BY_SYSTEM = AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM;\n\n  /**\n   * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link\n   * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link\n   * #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    AUDIOFOCUS_NONE,\n    AUDIOFOCUS_GAIN,\n    AUDIOFOCUS_GAIN_TRANSIENT,\n    AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,\n    AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE\n  })\n  public @interface AudioFocusGain {}\n  /** @see AudioManager#AUDIOFOCUS_NONE */\n  public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE;\n  /** @see AudioManager#AUDIOFOCUS_GAIN */\n  public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN;\n  /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */\n  public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;\n  /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */\n  public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK =\n      AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;\n  /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */\n  public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE =\n      AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;\n\n  /**\n   * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link\n   * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},\n   * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        BUFFER_FLAG_KEY_FRAME,\n        BUFFER_FLAG_END_OF_STREAM,\n        BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,\n        BUFFER_FLAG_LAST_SAMPLE,\n        BUFFER_FLAG_ENCRYPTED,\n        BUFFER_FLAG_DECODE_ONLY\n      })\n  public @interface BufferFlags {}\n  /**\n   * Indicates that a buffer holds a synchronization sample.\n   */\n  public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;\n  /**\n   * Flag for empty buffers that signal that the end of the stream was reached.\n   */\n  public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;\n  /** Indicates that a buffer has supplemental data. */\n  public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000\n  /** Indicates that a buffer is known to contain the last media sample of the stream. */\n  public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000\n  /** Indicates that a buffer is (at least partially) encrypted. */\n  public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000\n  /** Indicates that a buffer should be decoded but not rendered. */\n  public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000\n\n  // LINT.IfChange\n  /**\n   * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link\n   * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV})\n  public @interface VideoOutputMode {}\n  /** Video decoder output mode is not set. */\n  public static final int VIDEO_OUTPUT_MODE_NONE = -1;\n  /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */\n  public static final int VIDEO_OUTPUT_MODE_YUV = 0;\n  /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */\n  public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;\n  // LINT.ThenChange(\n  //     ../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc,\n  //     ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc\n  // )\n\n  /**\n   * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link\n   * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})\n  public @interface VideoScalingMode {}\n  /**\n   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT\n   */\n  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =\n      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;\n  /**\n   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT\n   */\n  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =\n      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;\n  /**\n   * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.\n   */\n  public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;\n\n  /**\n   * Track selection flags. Possible flag values are {@link #SELECTION_FLAG_DEFAULT}, {@link\n   * #SELECTION_FLAG_FORCED} and {@link #SELECTION_FLAG_AUTOSELECT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT})\n  public @interface SelectionFlags {}\n  /**\n   * Indicates that the track should be selected if user preferences do not state otherwise.\n   */\n  public static final int SELECTION_FLAG_DEFAULT = 1;\n  /** Indicates that the track must be displayed. Only applies to text tracks. */\n  public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2\n  /**\n   * Indicates that the player may choose to play the track in absence of an explicit user\n   * preference.\n   */\n  public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4\n\n  /** Represents an undetermined language as an ISO 639-2 language code. */\n  public static final String LANGUAGE_UNDETERMINED = \"und\";\n\n  /**\n   * Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link\n   * #TYPE_HLS} or {@link #TYPE_OTHER}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})\n  public @interface ContentType {}\n  /**\n   * Value returned by {@link Util#inferContentType(String)} for DASH manifests.\n   */\n  public static final int TYPE_DASH = 0;\n  /**\n   * Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests.\n   */\n  public static final int TYPE_SS = 1;\n  /**\n   * Value returned by {@link Util#inferContentType(String)} for HLS manifests.\n   */\n  public static final int TYPE_HLS = 2;\n  /**\n   * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or\n   * Smooth Streaming manifests.\n   */\n  public static final int TYPE_OTHER = 3;\n\n  /**\n   * A return value for methods where the end of an input was encountered.\n   */\n  public static final int RESULT_END_OF_INPUT = -1;\n  /**\n   * A return value for methods where the length of parsed data exceeds the maximum length allowed.\n   */\n  public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;\n  /**\n   * A return value for methods where nothing was read.\n   */\n  public static final int RESULT_NOTHING_READ = -3;\n  /**\n   * A return value for methods where a buffer was read.\n   */\n  public static final int RESULT_BUFFER_READ = -4;\n  /**\n   * A return value for methods where a format was read.\n   */\n  public static final int RESULT_FORMAT_READ = -5;\n\n  /** A data type constant for data of unknown or unspecified type. */\n  public static final int DATA_TYPE_UNKNOWN = 0;\n  /** A data type constant for media, typically containing media samples. */\n  public static final int DATA_TYPE_MEDIA = 1;\n  /** A data type constant for media, typically containing only initialization data. */\n  public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;\n  /** A data type constant for drm or encryption data. */\n  public static final int DATA_TYPE_DRM = 3;\n  /** A data type constant for a manifest file. */\n  public static final int DATA_TYPE_MANIFEST = 4;\n  /** A data type constant for time synchronization data. */\n  public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;\n  /** A data type constant for ads loader data. */\n  public static final int DATA_TYPE_AD = 6;\n  /**\n   * A data type constant for live progressive media streams, typically containing media samples.\n   */\n  public static final int DATA_TYPE_MEDIA_PROGRESSIVE_LIVE = 7;\n  /**\n   * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or\n   * equal to this value.\n   */\n  public static final int DATA_TYPE_CUSTOM_BASE = 10000;\n\n  /** A type constant for tracks of unknown type. */\n  public static final int TRACK_TYPE_UNKNOWN = -1;\n  /** A type constant for tracks of some default type, where the type itself is unknown. */\n  public static final int TRACK_TYPE_DEFAULT = 0;\n  /** A type constant for audio tracks. */\n  public static final int TRACK_TYPE_AUDIO = 1;\n  /** A type constant for video tracks. */\n  public static final int TRACK_TYPE_VIDEO = 2;\n  /** A type constant for text tracks. */\n  public static final int TRACK_TYPE_TEXT = 3;\n  /** A type constant for metadata tracks. */\n  public static final int TRACK_TYPE_METADATA = 4;\n  /** A type constant for camera motion tracks. */\n  public static final int TRACK_TYPE_CAMERA_MOTION = 5;\n  /** A type constant for a dummy or empty track. */\n  public static final int TRACK_TYPE_NONE = 6;\n  /**\n   * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or\n   * equal to this value.\n   */\n  public static final int TRACK_TYPE_CUSTOM_BASE = 10000;\n\n  /**\n   * A selection reason constant for selections whose reasons are unknown or unspecified.\n   */\n  public static final int SELECTION_REASON_UNKNOWN = 0;\n  /**\n   * A selection reason constant for an initial track selection.\n   */\n  public static final int SELECTION_REASON_INITIAL = 1;\n  /**\n   * A selection reason constant for an manual (i.e. user initiated) track selection.\n   */\n  public static final int SELECTION_REASON_MANUAL = 2;\n  /**\n   * A selection reason constant for an adaptive track selection.\n   */\n  public static final int SELECTION_REASON_ADAPTIVE = 3;\n  /**\n   * A selection reason constant for a trick play track selection.\n   */\n  public static final int SELECTION_REASON_TRICK_PLAY = 4;\n  /**\n   * Applications or extensions may define custom {@code SELECTION_REASON_*} constants greater than\n   * or equal to this value.\n   */\n  public static final int SELECTION_REASON_CUSTOM_BASE = 10000;\n\n  /** A default size in bytes for an individual allocation that forms part of a larger buffer. */\n  public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;\n\n  /** \"cenc\" scheme type name as defined in ISO/IEC 23001-7:2016. */\n  @SuppressWarnings(\"ConstantField\")\n  public static final String CENC_TYPE_cenc = \"cenc\";\n\n  /** \"cbc1\" scheme type name as defined in ISO/IEC 23001-7:2016. */\n  @SuppressWarnings(\"ConstantField\")\n  public static final String CENC_TYPE_cbc1 = \"cbc1\";\n\n  /** \"cens\" scheme type name as defined in ISO/IEC 23001-7:2016. */\n  @SuppressWarnings(\"ConstantField\")\n  public static final String CENC_TYPE_cens = \"cens\";\n\n  /** \"cbcs\" scheme type name as defined in ISO/IEC 23001-7:2016. */\n  @SuppressWarnings(\"ConstantField\")\n  public static final String CENC_TYPE_cbcs = \"cbcs\";\n\n  /**\n   * The Nil UUID as defined by\n   * <a href=\"https://tools.ietf.org/html/rfc4122#section-4.1.7\">RFC4122</a>.\n   */\n  public static final UUID UUID_NIL = new UUID(0L, 0L);\n\n  /**\n   * UUID for the W3C\n   * <a href=\"https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html\">Common PSSH\n   * box</a>.\n   */\n  public static final UUID COMMON_PSSH_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);\n\n  /**\n   * UUID for the ClearKey DRM scheme.\n   * <p>\n   * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.\n   */\n  public static final UUID CLEARKEY_UUID = new UUID(0xE2719D58A985B3C9L, 0x781AB030AF78D30EL);\n\n  /**\n   * UUID for the Widevine DRM scheme.\n   * <p>\n   * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.\n   */\n  public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);\n\n  /**\n   * UUID for the PlayReady DRM scheme.\n   * <p>\n   * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not\n   * provide PlayReady support.\n   */\n  public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);\n\n  /**\n   * The type of a message that can be passed to a video {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or\n   * null.\n   */\n  public static final int MSG_SET_SURFACE = 1;\n\n  /**\n   * A type of a message that can be passed to an audio {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Float} with 0 being\n   * silence and 1 being unity gain.\n   */\n  public static final int MSG_SET_VOLUME = 2;\n\n  /**\n   * A type of a message that can be passed to an audio {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be an {@link\n   * com.google.android.exoplayer2.audio.AudioAttributes} instance that will configure the\n   * underlying audio track. If not set, the default audio attributes will be used. They are\n   * suitable for general media playback.\n   *\n   * <p>Setting the audio attributes during playback may introduce a short gap in audio output as\n   * the audio track is recreated. A new audio session id will also be generated.\n   *\n   * <p>If tunneling is enabled by the track selector, the specified audio attributes will be\n   * ignored, but they will take effect if audio is later played without tunneling.\n   *\n   * <p>If the device is running a build before platform API version 21, audio attributes cannot be\n   * set directly on the underlying audio track. In this case, the usage will be mapped onto an\n   * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.\n   *\n   * <p>To get audio attributes that are equivalent to a legacy stream type, pass the stream type to\n   * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link AudioUsage} to build\n   * an audio attributes instance.\n   */\n  public static final int MSG_SET_AUDIO_ATTRIBUTES = 3;\n\n  /**\n   * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}\n   * via {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer\n   * scaling modes in {@link VideoScalingMode}.\n   *\n   * <p>Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is\n   * owned by a {@link android.view.SurfaceView}.\n   */\n  public static final int MSG_SET_SCALING_MODE = 4;\n\n  /**\n   * A type of a message that can be passed to an audio {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be an {@link AuxEffectInfo}\n   * instance representing an auxiliary audio effect for the underlying audio track.\n   */\n  public static final int MSG_SET_AUX_EFFECT_INFO = 5;\n\n  /**\n   * The type of a message that can be passed to a video {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be a {@link\n   * VideoFrameMetadataListener} instance, or null.\n   */\n  public static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 6;\n\n  /**\n   * The type of a message that can be passed to a camera motion {@link Renderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be a {@link CameraMotionListener}\n   * instance, or null.\n   */\n  public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7;\n\n  /**\n   * The type of a message that can be passed to a {@link SimpleDecoderVideoRenderer} via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link\n   * VideoDecoderOutputBufferRenderer}, or null.\n   *\n   * <p>This message is intended only for use with extension renderers that expect a {@link\n   * VideoDecoderOutputBufferRenderer}. For other use cases, an output surface should be passed via\n   * {@link #MSG_SET_SURFACE} instead.\n   */\n  public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8;\n\n  /**\n   * Applications or extensions may define custom {@code MSG_*} constants that can be passed to\n   * {@link Renderer}s. These custom constants must be greater than or equal to this value.\n   */\n  public static final int MSG_CUSTOM_BASE = 10000;\n\n  /**\n   * The stereo mode for 360/3D/VR videos. One of {@link Format#NO_VALUE}, {@link\n   * #STEREO_MODE_MONO}, {@link #STEREO_MODE_TOP_BOTTOM}, {@link #STEREO_MODE_LEFT_RIGHT} or {@link\n   * #STEREO_MODE_STEREO_MESH}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    Format.NO_VALUE,\n    STEREO_MODE_MONO,\n    STEREO_MODE_TOP_BOTTOM,\n    STEREO_MODE_LEFT_RIGHT,\n    STEREO_MODE_STEREO_MESH\n  })\n  public @interface StereoMode {}\n  /**\n   * Indicates Monoscopic stereo layout, used with 360/3D/VR videos.\n   */\n  public static final int STEREO_MODE_MONO = 0;\n  /**\n   * Indicates Top-Bottom stereo layout, used with 360/3D/VR videos.\n   */\n  public static final int STEREO_MODE_TOP_BOTTOM = 1;\n  /**\n   * Indicates Left-Right stereo layout, used with 360/3D/VR videos.\n   */\n  public static final int STEREO_MODE_LEFT_RIGHT = 2;\n  /**\n   * Indicates a stereo layout where the left and right eyes have separate meshes,\n   * used with 360/3D/VR videos.\n   */\n  public static final int STEREO_MODE_STEREO_MESH = 3;\n\n  /**\n   * Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link\n   * #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})\n  public @interface ColorSpace {}\n  /**\n   * @see MediaFormat#COLOR_STANDARD_BT709\n   */\n  public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;\n  /**\n   * @see MediaFormat#COLOR_STANDARD_BT601_PAL\n   */\n  public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;\n  /**\n   * @see MediaFormat#COLOR_STANDARD_BT2020\n   */\n  public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;\n\n  /**\n   * Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link\n   * #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG})\n  public @interface ColorTransfer {}\n  /**\n   * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO\n   */\n  public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;\n  /**\n   * @see MediaFormat#COLOR_TRANSFER_ST2084\n   */\n  public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;\n  /**\n   * @see MediaFormat#COLOR_TRANSFER_HLG\n   */\n  public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;\n\n  /**\n   * Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link\n   * #COLOR_RANGE_FULL}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})\n  public @interface ColorRange {}\n  /**\n   * @see MediaFormat#COLOR_RANGE_LIMITED\n   */\n  public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;\n  /**\n   * @see MediaFormat#COLOR_RANGE_FULL\n   */\n  public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;\n\n  /** Video projection types. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    Format.NO_VALUE,\n    PROJECTION_RECTANGULAR,\n    PROJECTION_EQUIRECTANGULAR,\n    PROJECTION_CUBEMAP,\n    PROJECTION_MESH\n  })\n  public @interface Projection {}\n  /** Conventional rectangular projection. */\n  public static final int PROJECTION_RECTANGULAR = 0;\n  /** Equirectangular spherical projection. */\n  public static final int PROJECTION_EQUIRECTANGULAR = 1;\n  /** Cube map projection. */\n  public static final int PROJECTION_CUBEMAP = 2;\n  /** 3-D mesh projection. */\n  public static final int PROJECTION_MESH = 3;\n\n  /**\n   * Priority for media playback.\n   *\n   * <p>Larger values indicate higher priorities.\n   */\n  public static final int PRIORITY_PLAYBACK = 0;\n\n  /**\n   * Priority for media downloading.\n   *\n   * <p>Larger values indicate higher priorities.\n   */\n  public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;\n\n  /**\n   * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE},\n   * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link\n   * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or\n   * {@link #NETWORK_TYPE_OTHER}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    NETWORK_TYPE_UNKNOWN,\n    NETWORK_TYPE_OFFLINE,\n    NETWORK_TYPE_WIFI,\n    NETWORK_TYPE_2G,\n    NETWORK_TYPE_3G,\n    NETWORK_TYPE_4G,\n    NETWORK_TYPE_CELLULAR_UNKNOWN,\n    NETWORK_TYPE_ETHERNET,\n    NETWORK_TYPE_OTHER\n  })\n  public @interface NetworkType {}\n  /** Unknown network type. */\n  public static final int NETWORK_TYPE_UNKNOWN = 0;\n  /** No network connection. */\n  public static final int NETWORK_TYPE_OFFLINE = 1;\n  /** Network type for a Wifi connection. */\n  public static final int NETWORK_TYPE_WIFI = 2;\n  /** Network type for a 2G cellular connection. */\n  public static final int NETWORK_TYPE_2G = 3;\n  /** Network type for a 3G cellular connection. */\n  public static final int NETWORK_TYPE_3G = 4;\n  /** Network type for a 4G cellular connection. */\n  public static final int NETWORK_TYPE_4G = 5;\n  /**\n   * Network type for cellular connections which cannot be mapped to one of {@link\n   * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.\n   */\n  public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;\n  /** Network type for an Ethernet connection. */\n  public static final int NETWORK_TYPE_ETHERNET = 7;\n  /**\n   * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,\n   * Bluetooth).\n   */\n  public static final int NETWORK_TYPE_OTHER = 8;\n\n  /**\n   * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link\n   * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link\n   * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link\n   * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link\n   * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY},\n   * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        ROLE_FLAG_MAIN,\n        ROLE_FLAG_ALTERNATE,\n        ROLE_FLAG_SUPPLEMENTARY,\n        ROLE_FLAG_COMMENTARY,\n        ROLE_FLAG_DUB,\n        ROLE_FLAG_EMERGENCY,\n        ROLE_FLAG_CAPTION,\n        ROLE_FLAG_SUBTITLE,\n        ROLE_FLAG_SIGN,\n        ROLE_FLAG_DESCRIBES_VIDEO,\n        ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND,\n        ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY,\n        ROLE_FLAG_TRANSCRIBES_DIALOG,\n        ROLE_FLAG_EASY_TO_READ\n      })\n  public @interface RoleFlags {}\n  /** Indicates a main track. */\n  public static final int ROLE_FLAG_MAIN = 1;\n  /**\n   * Indicates an alternate track. For example a video track recorded from an different view point\n   * than the main track(s).\n   */\n  public static final int ROLE_FLAG_ALTERNATE = 1 << 1;\n  /**\n   * Indicates a supplementary track, meaning the track has lower importance than the main track(s).\n   * For example a video track that provides a visual accompaniment to a main audio track.\n   */\n  public static final int ROLE_FLAG_SUPPLEMENTARY = 1 << 2;\n  /** Indicates the track contains commentary, for example from the director. */\n  public static final int ROLE_FLAG_COMMENTARY = 1 << 3;\n  /**\n   * Indicates the track is in a different language from the original, for example dubbed audio or\n   * translated captions.\n   */\n  public static final int ROLE_FLAG_DUB = 1 << 4;\n  /** Indicates the track contains information about a current emergency. */\n  public static final int ROLE_FLAG_EMERGENCY = 1 << 5;\n  /**\n   * Indicates the track contains captions. This flag may be set on video tracks to indicate the\n   * presence of burned in captions.\n   */\n  public static final int ROLE_FLAG_CAPTION = 1 << 6;\n  /**\n   * Indicates the track contains subtitles. This flag may be set on video tracks to indicate the\n   * presence of burned in subtitles.\n   */\n  public static final int ROLE_FLAG_SUBTITLE = 1 << 7;\n  /** Indicates the track contains a visual sign-language interpretation of an audio track. */\n  public static final int ROLE_FLAG_SIGN = 1 << 8;\n  /** Indicates the track contains an audio or textual description of a video track. */\n  public static final int ROLE_FLAG_DESCRIBES_VIDEO = 1 << 9;\n  /** Indicates the track contains a textual description of music and sound. */\n  public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1 << 10;\n  /** Indicates the track is designed for improved intelligibility of dialogue. */\n  public static final int ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY = 1 << 11;\n  /** Indicates the track contains a transcription of spoken dialog. */\n  public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12;\n  /** Indicates the track contains a text that has been edited for ease of reading. */\n  public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13;\n\n  /**\n   * Converts a time in microseconds to the corresponding time in milliseconds, preserving\n   * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.\n   *\n   * @param timeUs The time in microseconds.\n   * @return The corresponding time in milliseconds.\n   */\n  public static long usToMs(long timeUs) {\n    return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000);\n  }\n\n  /**\n   * Converts a time in milliseconds to the corresponding time in microseconds, preserving\n   * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values.\n   *\n   * @param timeMs The time in milliseconds.\n   * @return The corresponding time in microseconds.\n   */\n  public static long msToUs(long timeMs) {\n    return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);\n  }\n\n  /**\n   * Returns a newly generated audio session identifier, or {@link AudioManager#ERROR} if an error\n   * occurred in which case audio playback may fail.\n   *\n   * @see AudioManager#generateAudioSessionId()\n   */\n  @TargetApi(21)\n  public static int generateAudioSessionIdV21(Context context) {\n    return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))\n        .generateAudioSessionId();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport com.google.android.exoplayer2.Player.RepeatMode;\n\n/**\n * Dispatches operations to the {@link Player}.\n * <p>\n * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is\n * denied) or modify (e.g. change the seek position to prevent a user from seeking past a\n * non-skippable advert) operations.\n */\npublic interface ControlDispatcher {\n\n  /**\n   * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation.\n   *\n   * @param player The {@link Player} to which the operation should be dispatched.\n   * @param playWhenReady Whether playback should proceed when ready.\n   * @return True if the operation was dispatched. False if suppressed.\n   */\n  boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady);\n\n  /**\n   * Dispatches a {@link Player#seekTo(int, long)} operation.\n   *\n   * @param player The {@link Player} to which the operation should be dispatched.\n   * @param windowIndex The index of the window.\n   * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to\n   *     the window's default position.\n   * @return True if the operation was dispatched. False if suppressed.\n   */\n  boolean dispatchSeekTo(Player player, int windowIndex, long positionMs);\n\n  /**\n   * Dispatches a {@link Player#setRepeatMode(int)} operation.\n   *\n   * @param player The {@link Player} to which the operation should be dispatched.\n   * @param repeatMode The repeat mode.\n   * @return True if the operation was dispatched. False if suppressed.\n   */\n  boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode);\n\n  /**\n   * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation.\n   *\n   * @param player The {@link Player} to which the operation should be dispatched.\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return True if the operation was dispatched. False if suppressed.\n   */\n  boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled);\n\n  /**\n   * Dispatches a {@link Player#stop()} operation.\n   *\n   * @param player The {@link Player} to which the operation should be dispatched.\n   * @param reset Whether the player should be reset.\n   * @return True if the operation was dispatched. False if suppressed.\n   */\n  boolean dispatchStop(Player player, boolean reset);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport com.google.android.exoplayer2.Player.RepeatMode;\n\n/**\n * Default {@link ControlDispatcher} that dispatches all operations to the player without\n * modification.\n */\npublic class DefaultControlDispatcher implements ControlDispatcher {\n\n  @Override\n  public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {\n    player.setPlayWhenReady(playWhenReady);\n    return true;\n  }\n\n  @Override\n  public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {\n    player.seekTo(windowIndex, positionMs);\n    return true;\n  }\n\n  @Override\n  public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) {\n    player.setRepeatMode(repeatMode);\n    return true;\n  }\n\n  @Override\n  public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) {\n    player.setShuffleModeEnabled(shuffleModeEnabled);\n    return true;\n  }\n\n  @Override\n  public boolean dispatchStop(Player player, boolean reset) {\n    player.stop(reset);\n    return true;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DefaultAllocator;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * The default {@link LoadControl} implementation.\n */\npublic class DefaultLoadControl implements LoadControl {\n\n  /**\n   * The default minimum duration of media that the player will attempt to ensure is buffered at all\n   * times, in milliseconds. This value is only applied to playbacks without video.\n   */\n  public static final int DEFAULT_MIN_BUFFER_MS = 15000;\n\n  /**\n   * The default maximum duration of media that the player will attempt to buffer, in milliseconds.\n   * For playbacks with video, this is also the default minimum duration of media that the player\n   * will attempt to ensure is buffered.\n   */\n  public static final int DEFAULT_MAX_BUFFER_MS = 50000;\n\n  /**\n   * The default duration of media that must be buffered for playback to start or resume following a\n   * user action such as a seek, in milliseconds.\n   */\n  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;\n\n  /**\n   * The default duration of media that must be buffered for playback to resume after a rebuffer, in\n   * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.\n   */\n  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;\n\n  /**\n   * The default target buffer size in bytes. The value ({@link C#LENGTH_UNSET}) means that the load\n   * control will calculate the target buffer size based on the selected tracks.\n   */\n  public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET;\n\n  /** The default prioritization of buffer time constraints over size constraints. */\n  public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true;\n\n  /** The default back buffer duration in milliseconds. */\n  public static final int DEFAULT_BACK_BUFFER_DURATION_MS = 0;\n\n  /** The default for whether the back buffer is retained from the previous keyframe. */\n  public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false;\n\n  /** A default size in bytes for a video buffer. */\n  public static final int DEFAULT_VIDEO_BUFFER_SIZE = 500 * C.DEFAULT_BUFFER_SEGMENT_SIZE;\n\n  /** A default size in bytes for an audio buffer. */\n  public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * C.DEFAULT_BUFFER_SEGMENT_SIZE;\n\n  /** A default size in bytes for a text buffer. */\n  public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE;\n\n  /** A default size in bytes for a metadata buffer. */\n  public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE;\n\n  /** A default size in bytes for a camera motion buffer. */\n  public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE;\n\n  /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */\n  public static final int DEFAULT_MUXED_BUFFER_SIZE =\n      DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE;\n\n  /** Builder for {@link DefaultLoadControl}. */\n  public static final class Builder {\n\n    private DefaultAllocator allocator;\n    private int minBufferAudioMs;\n    private int minBufferVideoMs;\n    private int maxBufferMs;\n    private int bufferForPlaybackMs;\n    private int bufferForPlaybackAfterRebufferMs;\n    private int targetBufferBytes;\n    private boolean prioritizeTimeOverSizeThresholds;\n    private int backBufferDurationMs;\n    private boolean retainBackBufferFromKeyframe;\n    private boolean createDefaultLoadControlCalled;\n\n    /** Constructs a new instance. */\n    public Builder() {\n      minBufferAudioMs = DEFAULT_MIN_BUFFER_MS;\n      minBufferVideoMs = DEFAULT_MAX_BUFFER_MS;\n      maxBufferMs = DEFAULT_MAX_BUFFER_MS;\n      bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS;\n      bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;\n      targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES;\n      prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;\n      backBufferDurationMs = DEFAULT_BACK_BUFFER_DURATION_MS;\n      retainBackBufferFromKeyframe = DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME;\n    }\n\n    /**\n     * Sets the {@link DefaultAllocator} used by the loader.\n     *\n     * @param allocator The {@link DefaultAllocator}.\n     * @return This builder, for convenience.\n     * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.\n     */\n    public Builder setAllocator(DefaultAllocator allocator) {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      this.allocator = allocator;\n      return this;\n    }\n\n    /**\n     * Sets the buffer duration parameters.\n     *\n     * @param minBufferMs The minimum duration of media that the player will attempt to ensure is\n     *     buffered at all times, in milliseconds.\n     * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in\n     *     milliseconds.\n     * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start\n     *     or resume following a user action such as a seek, in milliseconds.\n     * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered\n     *     for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be\n     *     caused by buffer depletion rather than a user action.\n     * @return This builder, for convenience.\n     * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.\n     */\n    public Builder setBufferDurationsMs(\n        int minBufferMs,\n        int maxBufferMs,\n        int bufferForPlaybackMs,\n        int bufferForPlaybackAfterRebufferMs) {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      assertGreaterOrEqual(bufferForPlaybackMs, 0, \"bufferForPlaybackMs\", \"0\");\n      assertGreaterOrEqual(\n          bufferForPlaybackAfterRebufferMs, 0, \"bufferForPlaybackAfterRebufferMs\", \"0\");\n      assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, \"minBufferMs\", \"bufferForPlaybackMs\");\n      assertGreaterOrEqual(\n          minBufferMs,\n          bufferForPlaybackAfterRebufferMs,\n          \"minBufferMs\",\n          \"bufferForPlaybackAfterRebufferMs\");\n      assertGreaterOrEqual(maxBufferMs, minBufferMs, \"maxBufferMs\", \"minBufferMs\");\n      this.minBufferAudioMs = minBufferMs;\n      this.minBufferVideoMs = minBufferMs;\n      this.maxBufferMs = maxBufferMs;\n      this.bufferForPlaybackMs = bufferForPlaybackMs;\n      this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;\n      return this;\n    }\n\n    /**\n     * Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer\n     * size will be calculated based on the selected tracks.\n     *\n     * @param targetBufferBytes The target buffer size in bytes.\n     * @return This builder, for convenience.\n     * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.\n     */\n    public Builder setTargetBufferBytes(int targetBufferBytes) {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      this.targetBufferBytes = targetBufferBytes;\n      return this;\n    }\n\n    /**\n     * Sets whether the load control prioritizes buffer time constraints over buffer size\n     * constraints.\n     *\n     * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time\n     *     constraints over buffer size constraints.\n     * @return This builder, for convenience.\n     * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.\n     */\n    public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;\n      return this;\n    }\n\n    /**\n     * Sets the back buffer duration, and whether the back buffer is retained from the previous\n     * keyframe.\n     *\n     * @param backBufferDurationMs The back buffer duration in milliseconds.\n     * @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous\n     *     keyframe.\n     * @return This builder, for convenience.\n     * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.\n     */\n    public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      assertGreaterOrEqual(backBufferDurationMs, 0, \"backBufferDurationMs\", \"0\");\n      this.backBufferDurationMs = backBufferDurationMs;\n      this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;\n      return this;\n    }\n\n    /** Creates a {@link DefaultLoadControl}. */\n    public DefaultLoadControl createDefaultLoadControl() {\n      Assertions.checkState(!createDefaultLoadControlCalled);\n      createDefaultLoadControlCalled = true;\n      if (allocator == null) {\n        allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);\n      }\n      return new DefaultLoadControl(\n          allocator,\n          minBufferAudioMs,\n          minBufferVideoMs,\n          maxBufferMs,\n          bufferForPlaybackMs,\n          bufferForPlaybackAfterRebufferMs,\n          targetBufferBytes,\n          prioritizeTimeOverSizeThresholds,\n          backBufferDurationMs,\n          retainBackBufferFromKeyframe);\n    }\n  }\n\n  private final DefaultAllocator allocator;\n\n  private final long minBufferAudioUs;\n  private final long minBufferVideoUs;\n  private final long maxBufferUs;\n  private final long bufferForPlaybackUs;\n  private final long bufferForPlaybackAfterRebufferUs;\n  private final int targetBufferBytesOverwrite;\n  private final boolean prioritizeTimeOverSizeThresholds;\n  private final long backBufferDurationUs;\n  private final boolean retainBackBufferFromKeyframe;\n\n  private int targetBufferSize;\n  private boolean isBuffering;\n  private boolean hasVideo;\n\n  /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */\n  @SuppressWarnings(\"deprecation\")\n  public DefaultLoadControl() {\n    this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));\n  }\n\n  /** @deprecated Use {@link Builder} instead. */\n  @Deprecated\n  public DefaultLoadControl(DefaultAllocator allocator) {\n    this(\n        allocator,\n        /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS,\n        /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS,\n        DEFAULT_MAX_BUFFER_MS,\n        DEFAULT_BUFFER_FOR_PLAYBACK_MS,\n        DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,\n        DEFAULT_TARGET_BUFFER_BYTES,\n        DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS,\n        DEFAULT_BACK_BUFFER_DURATION_MS,\n        DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);\n  }\n\n  /** @deprecated Use {@link Builder} instead. */\n  @Deprecated\n  public DefaultLoadControl(\n      DefaultAllocator allocator,\n      int minBufferMs,\n      int maxBufferMs,\n      int bufferForPlaybackMs,\n      int bufferForPlaybackAfterRebufferMs,\n      int targetBufferBytes,\n      boolean prioritizeTimeOverSizeThresholds) {\n    this(\n        allocator,\n        /* minBufferAudioMs= */ minBufferMs,\n        /* minBufferVideoMs= */ minBufferMs,\n        maxBufferMs,\n        bufferForPlaybackMs,\n        bufferForPlaybackAfterRebufferMs,\n        targetBufferBytes,\n        prioritizeTimeOverSizeThresholds,\n        DEFAULT_BACK_BUFFER_DURATION_MS,\n        DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);\n  }\n\n  protected DefaultLoadControl(\n      DefaultAllocator allocator,\n      int minBufferAudioMs,\n      int minBufferVideoMs,\n      int maxBufferMs,\n      int bufferForPlaybackMs,\n      int bufferForPlaybackAfterRebufferMs,\n      int targetBufferBytes,\n      boolean prioritizeTimeOverSizeThresholds,\n      int backBufferDurationMs,\n      boolean retainBackBufferFromKeyframe) {\n    assertGreaterOrEqual(bufferForPlaybackMs, 0, \"bufferForPlaybackMs\", \"0\");\n    assertGreaterOrEqual(\n        bufferForPlaybackAfterRebufferMs, 0, \"bufferForPlaybackAfterRebufferMs\", \"0\");\n    assertGreaterOrEqual(\n        minBufferAudioMs, bufferForPlaybackMs, \"minBufferAudioMs\", \"bufferForPlaybackMs\");\n    assertGreaterOrEqual(\n        minBufferVideoMs, bufferForPlaybackMs, \"minBufferVideoMs\", \"bufferForPlaybackMs\");\n    assertGreaterOrEqual(\n        minBufferAudioMs,\n        bufferForPlaybackAfterRebufferMs,\n        \"minBufferAudioMs\",\n        \"bufferForPlaybackAfterRebufferMs\");\n    assertGreaterOrEqual(\n        minBufferVideoMs,\n        bufferForPlaybackAfterRebufferMs,\n        \"minBufferVideoMs\",\n        \"bufferForPlaybackAfterRebufferMs\");\n    assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, \"maxBufferMs\", \"minBufferAudioMs\");\n    assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, \"maxBufferMs\", \"minBufferVideoMs\");\n    assertGreaterOrEqual(backBufferDurationMs, 0, \"backBufferDurationMs\", \"0\");\n\n    this.allocator = allocator;\n    this.minBufferAudioUs = C.msToUs(minBufferAudioMs);\n    this.minBufferVideoUs = C.msToUs(minBufferVideoMs);\n    this.maxBufferUs = C.msToUs(maxBufferMs);\n    this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs);\n    this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs);\n    this.targetBufferBytesOverwrite = targetBufferBytes;\n    this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;\n    this.backBufferDurationUs = C.msToUs(backBufferDurationMs);\n    this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;\n  }\n\n  @Override\n  public void onPrepared() {\n    reset(false);\n  }\n\n  @Override\n  public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,\n      TrackSelectionArray trackSelections) {\n    hasVideo = hasVideo(renderers, trackSelections);\n    targetBufferSize =\n        targetBufferBytesOverwrite == C.LENGTH_UNSET\n            ? calculateTargetBufferSize(renderers, trackSelections)\n            : targetBufferBytesOverwrite;\n    allocator.setTargetBufferSize(targetBufferSize);\n  }\n\n  @Override\n  public void onStopped() {\n    reset(true);\n  }\n\n  @Override\n  public void onReleased() {\n    reset(true);\n  }\n\n  @Override\n  public Allocator getAllocator() {\n    return allocator;\n  }\n\n  @Override\n  public long getBackBufferDurationUs() {\n    return backBufferDurationUs;\n  }\n\n  @Override\n  public boolean retainBackBufferFromKeyframe() {\n    return retainBackBufferFromKeyframe;\n  }\n\n  @Override\n  public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {\n    boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;\n    long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs;\n    if (playbackSpeed > 1) {\n      // The playback speed is faster than real time, so scale up the minimum required media\n      // duration to keep enough media buffered for a playout duration of minBufferUs.\n      long mediaDurationMinBufferUs =\n          Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed);\n      minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs);\n    }\n    if (bufferedDurationUs < minBufferUs) {\n      isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;\n    } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {\n      isBuffering = false;\n    } // Else don't change the buffering state\n    return isBuffering;\n  }\n\n  @Override\n  public boolean shouldStartPlayback(\n      long bufferedDurationUs, float playbackSpeed, boolean rebuffering) {\n    bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed);\n    long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;\n    return minBufferDurationUs <= 0\n        || bufferedDurationUs >= minBufferDurationUs\n        || (!prioritizeTimeOverSizeThresholds\n            && allocator.getTotalBytesAllocated() >= targetBufferSize);\n  }\n\n  /**\n   * Calculate target buffer size in bytes based on the selected tracks. The player will try not to\n   * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.\n   *\n   * @param renderers The renderers for which the track were selected.\n   * @param trackSelectionArray The selected tracks.\n   * @return The target buffer size in bytes.\n   */\n  protected int calculateTargetBufferSize(\n      Renderer[] renderers, TrackSelectionArray trackSelectionArray) {\n    int targetBufferSize = 0;\n    for (int i = 0; i < renderers.length; i++) {\n      if (trackSelectionArray.get(i) != null) {\n        targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType());\n      }\n    }\n    return targetBufferSize;\n  }\n\n  private void reset(boolean resetAllocator) {\n    targetBufferSize = 0;\n    isBuffering = false;\n    if (resetAllocator) {\n      allocator.reset();\n    }\n  }\n\n  private static int getDefaultBufferSize(int trackType) {\n    switch (trackType) {\n      case C.TRACK_TYPE_DEFAULT:\n        return DEFAULT_MUXED_BUFFER_SIZE;\n      case C.TRACK_TYPE_AUDIO:\n        return DEFAULT_AUDIO_BUFFER_SIZE;\n      case C.TRACK_TYPE_VIDEO:\n        return DEFAULT_VIDEO_BUFFER_SIZE;\n      case C.TRACK_TYPE_TEXT:\n        return DEFAULT_TEXT_BUFFER_SIZE;\n      case C.TRACK_TYPE_METADATA:\n        return DEFAULT_METADATA_BUFFER_SIZE;\n      case C.TRACK_TYPE_CAMERA_MOTION:\n        return DEFAULT_CAMERA_MOTION_BUFFER_SIZE;\n      case C.TRACK_TYPE_NONE:\n        return 0;\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) {\n    for (int i = 0; i < renderers.length; i++) {\n      if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) {\n    Assertions.checkArgument(value1 >= value2, name1 + \" cannot be less than \" + name2);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport com.google.android.exoplayer2.util.StandaloneMediaClock;\n\n/**\n * Default {@link MediaClock} which uses a renderer media clock and falls back to a\n * {@link StandaloneMediaClock} if necessary.\n */\n/* package */ final class DefaultMediaClock implements MediaClock {\n\n  /**\n   * Listener interface to be notified of changes to the active playback parameters.\n   */\n  public interface PlaybackParameterListener {\n\n    /**\n     * Called when the active playback parameters changed. Will not be called for {@link\n     * #setPlaybackParameters(PlaybackParameters)}.\n     *\n     * @param newPlaybackParameters The newly active {@link PlaybackParameters}.\n     */\n    void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);\n  }\n\n  private final StandaloneMediaClock standaloneClock;\n  private final PlaybackParameterListener listener;\n\n  @Nullable private Renderer rendererClockSource;\n  @Nullable private MediaClock rendererClock;\n  private boolean isUsingStandaloneClock;\n  private boolean standaloneClockIsStarted;\n\n  /**\n   * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use\n   * for the standalone clock implementation.\n   *\n   * @param listener A {@link PlaybackParameterListener} to listen for playback parameter\n   *     changes.\n   * @param clock A {@link Clock}.\n   */\n  public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) {\n    this.listener = listener;\n    this.standaloneClock = new StandaloneMediaClock(clock);\n    isUsingStandaloneClock = true;\n  }\n\n  /**\n   * Starts the standalone fallback clock.\n   */\n  public void start() {\n    standaloneClockIsStarted = true;\n    standaloneClock.start();\n  }\n\n  /**\n   * Stops the standalone fallback clock.\n   */\n  public void stop() {\n    standaloneClockIsStarted = false;\n    standaloneClock.stop();\n  }\n\n  /**\n   * Resets the position of the standalone fallback clock.\n   *\n   * @param positionUs The position to set in microseconds.\n   */\n  public void resetPosition(long positionUs) {\n    standaloneClock.resetPosition(positionUs);\n  }\n\n  /**\n   * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the\n   * provided renderer if available.\n   *\n   * @param renderer The renderer which has been enabled.\n   * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media\n   *     clock is already provided.\n   */\n  public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException {\n    MediaClock rendererMediaClock = renderer.getMediaClock();\n    if (rendererMediaClock != null && rendererMediaClock != rendererClock) {\n      if (rendererClock != null) {\n        throw ExoPlaybackException.createForUnexpected(\n            new IllegalStateException(\"Multiple renderer media clocks enabled.\"));\n      }\n      this.rendererClock = rendererMediaClock;\n      this.rendererClockSource = renderer;\n      rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());\n    }\n  }\n\n  /**\n   * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this\n   * renderer if used.\n   *\n   * @param renderer The renderer which has been disabled.\n   */\n  public void onRendererDisabled(Renderer renderer) {\n    if (renderer == rendererClockSource) {\n      this.rendererClock = null;\n      this.rendererClockSource = null;\n      isUsingStandaloneClock = true;\n    }\n  }\n\n  /**\n   * Syncs internal clock if needed and returns current clock position in microseconds.\n   *\n   * @param isReadingAhead Whether the renderers are reading ahead.\n   */\n  public long syncAndGetPositionUs(boolean isReadingAhead) {\n    syncClocks(isReadingAhead);\n    return getPositionUs();\n  }\n\n  // MediaClock implementation.\n\n  @Override\n  public long getPositionUs() {\n    return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs();\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    if (rendererClock != null) {\n      rendererClock.setPlaybackParameters(playbackParameters);\n      playbackParameters = rendererClock.getPlaybackParameters();\n    }\n    standaloneClock.setPlaybackParameters(playbackParameters);\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return rendererClock != null\n        ? rendererClock.getPlaybackParameters()\n        : standaloneClock.getPlaybackParameters();\n  }\n\n  private void syncClocks(boolean isReadingAhead) {\n    if (shouldUseStandaloneClock(isReadingAhead)) {\n      isUsingStandaloneClock = true;\n      if (standaloneClockIsStarted) {\n        standaloneClock.start();\n      }\n      return;\n    }\n    long rendererClockPositionUs = rendererClock.getPositionUs();\n    if (isUsingStandaloneClock) {\n      // Ensure enabling the renderer clock doesn't jump backwards in time.\n      if (rendererClockPositionUs < standaloneClock.getPositionUs()) {\n        standaloneClock.stop();\n        return;\n      }\n      isUsingStandaloneClock = false;\n      if (standaloneClockIsStarted) {\n        standaloneClock.start();\n      }\n    }\n    // Continuously sync stand-alone clock to renderer clock so that it can take over if needed.\n    standaloneClock.resetPosition(rendererClockPositionUs);\n    PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();\n    if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {\n      standaloneClock.setPlaybackParameters(playbackParameters);\n      listener.onPlaybackParametersChanged(playbackParameters);\n    }\n  }\n\n  private boolean shouldUseStandaloneClock(boolean isReadingAhead) {\n    // Use the standalone clock if the clock providing renderer is not set or has ended. Also use\n    // the standalone clock if the renderer is not ready and we have finished reading the stream or\n    // are reading ahead to avoid getting stuck if tracks in the current period have uneven\n    // durations. See: https://github.com/google/ExoPlayer/issues/1874.\n    return rendererClockSource == null\n        || rendererClockSource.isEnded()\n        || (!rendererClockSource.isReady()\n            && (isReadingAhead || rendererClockSource.hasReadStreamToEnd()));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.audio.AudioCapabilities;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.DefaultAudioSink;\nimport com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.metadata.MetadataRenderer;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.text.TextRenderer;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport com.google.android.exoplayer2.video.spherical.CameraMotionRenderer;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.reflect.Constructor;\nimport java.util.ArrayList;\n\n/**\n * Default {@link RenderersFactory} implementation.\n */\npublic class DefaultRenderersFactory implements RenderersFactory {\n\n  /**\n   * The default maximum duration for which a video renderer can attempt to seamlessly join an\n   * ongoing playback.\n   */\n  public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000;\n\n  /**\n   * Modes for using extension renderers. One of {@link #EXTENSION_RENDERER_MODE_OFF}, {@link\n   * #EXTENSION_RENDERER_MODE_ON} or {@link #EXTENSION_RENDERER_MODE_PREFER}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER})\n  public @interface ExtensionRendererMode {}\n  /**\n   * Do not allow use of extension renderers.\n   */\n  public static final int EXTENSION_RENDERER_MODE_OFF = 0;\n  /**\n   * Allow use of extension renderers. Extension renderers are indexed after core renderers of the\n   * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore\n   * prefer to use a core renderer to an extension renderer in the case that both are able to play\n   * a given track.\n   */\n  public static final int EXTENSION_RENDERER_MODE_ON = 1;\n  /**\n   * Allow use of extension renderers. Extension renderers are indexed before core renderers of the\n   * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore\n   * prefer to use an extension renderer to a core renderer in the case that both are able to play\n   * a given track.\n   */\n  public static final int EXTENSION_RENDERER_MODE_PREFER = 2;\n\n  private static final String TAG = \"DefaultRenderersFactory\";\n\n  protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;\n\n  private final Context context;\n  @Nullable private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;\n  @ExtensionRendererMode private int extensionRendererMode;\n  private long allowedVideoJoiningTimeMs;\n  private boolean playClearSamplesWithoutKeys;\n  private boolean enableDecoderFallback;\n  private MediaCodecSelector mediaCodecSelector;\n\n  /** @param context A {@link Context}. */\n  public DefaultRenderersFactory(Context context) {\n    this.context = context;\n    extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;\n    allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;\n    mediaCodecSelector = MediaCodecSelector.DEFAULT;\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager}\n   *     directly to {@link SimpleExoPlayer.Builder}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultRenderersFactory(\n      Context context, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link\n   *     #setExtensionRendererMode(int)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultRenderersFactory(\n      Context context, @ExtensionRendererMode int extensionRendererMode) {\n    this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link\n   *     #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link\n   *     SimpleExoPlayer.Builder}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultRenderersFactory(\n      Context context,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      @ExtensionRendererMode int extensionRendererMode) {\n    this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link\n   *     #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultRenderersFactory(\n      Context context,\n      @ExtensionRendererMode int extensionRendererMode,\n      long allowedVideoJoiningTimeMs) {\n    this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link\n   *     #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass\n   *     {@link DrmSessionManager} directly to {@link SimpleExoPlayer.Builder}.\n   */\n  @Deprecated\n  public DefaultRenderersFactory(\n      Context context,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      @ExtensionRendererMode int extensionRendererMode,\n      long allowedVideoJoiningTimeMs) {\n    this.context = context;\n    this.extensionRendererMode = extensionRendererMode;\n    this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;\n    this.drmSessionManager = drmSessionManager;\n    mediaCodecSelector = MediaCodecSelector.DEFAULT;\n  }\n\n  /**\n   * Sets the extension renderer mode, which determines if and how available extension renderers are\n   * used. Note that extensions must be included in the application build for them to be considered\n   * available.\n   *\n   * <p>The default value is {@link #EXTENSION_RENDERER_MODE_OFF}.\n   *\n   * @param extensionRendererMode The extension renderer mode.\n   * @return This factory, for convenience.\n   */\n  public DefaultRenderersFactory setExtensionRendererMode(\n      @ExtensionRendererMode int extensionRendererMode) {\n    this.extensionRendererMode = extensionRendererMode;\n    return this;\n  }\n\n  /**\n   * Sets whether renderers are permitted to play clear regions of encrypted media prior to having\n   * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that\n   * starts with a short clear region, this allows playback to begin in parallel with key\n   * acquisition, which can reduce startup latency.\n   *\n   * <p>The default value is {@code false}.\n   *\n   * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of\n   *     encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of\n   *     the media.\n   * @return This factory, for convenience.\n   */\n  public DefaultRenderersFactory setPlayClearSamplesWithoutKeys(\n      boolean playClearSamplesWithoutKeys) {\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    return this;\n  }\n\n  /**\n   * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.\n   * This may result in using a decoder that is less efficient or slower than the primary decoder.\n   *\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails.\n   * @return This factory, for convenience.\n   */\n  public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) {\n    this.enableDecoderFallback = enableDecoderFallback;\n    return this;\n  }\n\n  /**\n   * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.\n   *\n   * <p>The default value is {@link MediaCodecSelector#DEFAULT}.\n   *\n   * @param mediaCodecSelector The {@link MediaCodecSelector}.\n   * @return This factory, for convenience.\n   */\n  public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) {\n    this.mediaCodecSelector = mediaCodecSelector;\n    return this;\n  }\n\n  /**\n   * Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing\n   * playback.\n   *\n   * <p>The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}.\n   *\n   * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to\n   *     seamlessly join an ongoing playback, in milliseconds.\n   * @return This factory, for convenience.\n   */\n  public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) {\n    this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;\n    return this;\n  }\n\n  @Override\n  public Renderer[] createRenderers(\n      Handler eventHandler,\n      VideoRendererEventListener videoRendererEventListener,\n      AudioRendererEventListener audioRendererEventListener,\n      TextOutput textRendererOutput,\n      MetadataOutput metadataRendererOutput,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    if (drmSessionManager == null) {\n      drmSessionManager = this.drmSessionManager;\n    }\n    ArrayList<Renderer> renderersList = new ArrayList<>();\n    buildVideoRenderers(\n        context,\n        extensionRendererMode,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        enableDecoderFallback,\n        eventHandler,\n        videoRendererEventListener,\n        allowedVideoJoiningTimeMs,\n        renderersList);\n    buildAudioRenderers(\n        context,\n        extensionRendererMode,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        enableDecoderFallback,\n        buildAudioProcessors(),\n        eventHandler,\n        audioRendererEventListener,\n        renderersList);\n    buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),\n        extensionRendererMode, renderersList);\n    buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),\n        extensionRendererMode, renderersList);\n    buildCameraMotionRenderers(context, extensionRendererMode, renderersList);\n    buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);\n    return renderersList.toArray(new Renderer[0]);\n  }\n\n  /**\n   * Builds video renderers for use by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will\n   *     not be used for DRM protected playbacks.\n   * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of\n   *     encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of\n   *     the media.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param eventHandler A handler associated with the main thread's looper.\n   * @param eventListener An event listener.\n   * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to\n   *     seamlessly join an ongoing playback, in milliseconds.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildVideoRenderers(\n      Context context,\n      @ExtensionRendererMode int extensionRendererMode,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean enableDecoderFallback,\n      Handler eventHandler,\n      VideoRendererEventListener eventListener,\n      long allowedVideoJoiningTimeMs,\n      ArrayList<Renderer> out) {\n    out.add(\n        new MediaCodecVideoRenderer(\n            context,\n            mediaCodecSelector,\n            allowedVideoJoiningTimeMs,\n            drmSessionManager,\n            playClearSamplesWithoutKeys,\n            enableDecoderFallback,\n            eventHandler,\n            eventListener,\n            MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));\n\n    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {\n      return;\n    }\n    int extensionRendererIndex = out.size();\n    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {\n      extensionRendererIndex--;\n    }\n\n    try {\n      // Full class names used for constructor args so the LINT rule triggers if any of them move.\n      // LINT.IfChange\n      Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer\");\n      Constructor<?> constructor =\n          clazz.getConstructor(\n              long.class,\n              Handler.class,\n              VideoRendererEventListener.class,\n              int.class);\n      // LINT.ThenChange(../../../../../../../proguard-rules.txt)\n      Renderer renderer =\n          (Renderer)\n              constructor.newInstance(\n                  allowedVideoJoiningTimeMs,\n                  eventHandler,\n                  eventListener,\n                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\n      out.add(extensionRendererIndex++, renderer);\n      Log.i(TAG, \"Loaded LibvpxVideoRenderer.\");\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the extension.\n    } catch (Exception e) {\n      // The extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating VP9 extension\", e);\n    }\n\n    try {\n      // Full class names used for constructor args so the LINT rule triggers if any of them move.\n      // LINT.IfChange\n      Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer\");\n      Constructor<?> constructor =\n          clazz.getConstructor(\n              long.class,\n              Handler.class,\n              VideoRendererEventListener.class,\n              int.class);\n      // LINT.ThenChange(../../../../../../../proguard-rules.txt)\n      Renderer renderer =\n          (Renderer)\n              constructor.newInstance(\n                  allowedVideoJoiningTimeMs,\n                  eventHandler,\n                  eventListener,\n                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\n      out.add(extensionRendererIndex++, renderer);\n      Log.i(TAG, \"Loaded Libgav1VideoRenderer.\");\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the extension.\n    } catch (Exception e) {\n      // The extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating AV1 extension\", e);\n    }\n  }\n\n  /**\n   * Builds audio renderers for use by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will\n   *     not be used for DRM protected playbacks.\n   * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of\n   *     encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of\n   *     the media.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers\n   *     before output. May be empty.\n   * @param eventHandler A handler to use when invoking event listeners and outputs.\n   * @param eventListener An event listener.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildAudioRenderers(\n      Context context,\n      @ExtensionRendererMode int extensionRendererMode,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean enableDecoderFallback,\n      AudioProcessor[] audioProcessors,\n      Handler eventHandler,\n      AudioRendererEventListener eventListener,\n      ArrayList<Renderer> out) {\n    out.add(\n        new MediaCodecAudioRenderer(\n            context,\n            mediaCodecSelector,\n            drmSessionManager,\n            playClearSamplesWithoutKeys,\n            enableDecoderFallback,\n            eventHandler,\n            eventListener,\n            new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));\n\n    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {\n      return;\n    }\n    int extensionRendererIndex = out.size();\n    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {\n      extensionRendererIndex--;\n    }\n\n    try {\n      // Full class names used for constructor args so the LINT rule triggers if any of them move.\n      // LINT.IfChange\n      Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer\");\n      Constructor<?> constructor =\n          clazz.getConstructor(\n              Handler.class,\n              AudioRendererEventListener.class,\n              AudioProcessor[].class);\n      // LINT.ThenChange(../../../../../../../proguard-rules.txt)\n      Renderer renderer =\n          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n      out.add(extensionRendererIndex++, renderer);\n      Log.i(TAG, \"Loaded LibopusAudioRenderer.\");\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the extension.\n    } catch (Exception e) {\n      // The extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating Opus extension\", e);\n    }\n\n    try {\n      // Full class names used for constructor args so the LINT rule triggers if any of them move.\n      // LINT.IfChange\n      Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer\");\n      Constructor<?> constructor =\n          clazz.getConstructor(\n              Handler.class,\n              AudioRendererEventListener.class,\n              AudioProcessor[].class);\n      // LINT.ThenChange(../../../../../../../proguard-rules.txt)\n      Renderer renderer =\n          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n      out.add(extensionRendererIndex++, renderer);\n      Log.i(TAG, \"Loaded LibflacAudioRenderer.\");\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the extension.\n    } catch (Exception e) {\n      // The extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating FLAC extension\", e);\n    }\n\n    try {\n      // Full class names used for constructor args so the LINT rule triggers if any of them move.\n      // LINT.IfChange\n      Class<?> clazz =\n          Class.forName(\"com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer\");\n      Constructor<?> constructor =\n          clazz.getConstructor(\n              Handler.class,\n              AudioRendererEventListener.class,\n              AudioProcessor[].class);\n      // LINT.ThenChange(../../../../../../../proguard-rules.txt)\n      Renderer renderer =\n          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n      out.add(extensionRendererIndex++, renderer);\n      Log.i(TAG, \"Loaded FfmpegAudioRenderer.\");\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the extension.\n    } catch (Exception e) {\n      // The extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating FFmpeg extension\", e);\n    }\n  }\n\n  /**\n   * Builds text renderers for use by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param output An output for the renderers.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildTextRenderers(\n      Context context,\n      TextOutput output,\n      Looper outputLooper,\n      @ExtensionRendererMode int extensionRendererMode,\n      ArrayList<Renderer> out) {\n    out.add(new TextRenderer(output, outputLooper));\n  }\n\n  /**\n   * Builds metadata renderers for use by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param output An output for the renderers.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildMetadataRenderers(\n      Context context,\n      MetadataOutput output,\n      Looper outputLooper,\n      @ExtensionRendererMode int extensionRendererMode,\n      ArrayList<Renderer> out) {\n    out.add(new MetadataRenderer(output, outputLooper));\n  }\n\n  /**\n   * Builds camera motion renderers for use by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildCameraMotionRenderers(\n      Context context, @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {\n    out.add(new CameraMotionRenderer());\n  }\n\n  /**\n   * Builds any miscellaneous renderers used by the player.\n   *\n   * @param context The {@link Context} associated with the player.\n   * @param eventHandler A handler to use when invoking event listeners and outputs.\n   * @param extensionRendererMode The extension renderer mode.\n   * @param out An array to which the built renderers should be appended.\n   */\n  protected void buildMiscellaneousRenderers(Context context, Handler eventHandler,\n      @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {\n    // Do nothing.\n  }\n\n  /**\n   * Builds an array of {@link AudioProcessor}s that will process PCM audio before output.\n   */\n  protected AudioProcessor[] buildAudioProcessors() {\n    return new AudioProcessor[0];\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.RendererCapabilities.FormatSupport;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Thrown when a non-recoverable playback failure occurs.\n */\npublic final class ExoPlaybackException extends Exception {\n\n  /**\n   * The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER}\n   * {@link #TYPE_UNEXPECTED}, {@link #TYPE_REMOTE} or {@link #TYPE_OUT_OF_MEMORY}. Note that new\n   * types may be added in the future and error handling should handle unknown type values.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE, TYPE_OUT_OF_MEMORY})\n  public @interface Type {}\n  /**\n   * The error occurred loading data from a {@link MediaSource}.\n   * <p>\n   * Call {@link #getSourceException()} to retrieve the underlying cause.\n   */\n  public static final int TYPE_SOURCE = 0;\n  /**\n   * The error occurred in a {@link Renderer}.\n   * <p>\n   * Call {@link #getRendererException()} to retrieve the underlying cause.\n   */\n  public static final int TYPE_RENDERER = 1;\n  /**\n   * The error was an unexpected {@link RuntimeException}.\n   * <p>\n   * Call {@link #getUnexpectedException()} to retrieve the underlying cause.\n   */\n  public static final int TYPE_UNEXPECTED = 2;\n  /**\n   * The error occurred in a remote component.\n   *\n   * <p>Call {@link #getMessage()} to retrieve the message associated with the error.\n   */\n  public static final int TYPE_REMOTE = 3;\n  /** The error was an {@link OutOfMemoryError}. */\n  public static final int TYPE_OUT_OF_MEMORY = 4;\n\n  /** The {@link Type} of the playback failure. */\n  @Type public final int type;\n\n  /**\n   * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.\n   */\n  public final int rendererIndex;\n\n  /**\n   * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using\n   * at the time of the exception, or null if the renderer wasn't using a {@link Format}.\n   */\n  @Nullable public final Format rendererFormat;\n\n  /**\n   * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the\n   * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link\n   * RendererCapabilities#FORMAT_HANDLED}.\n   */\n  @FormatSupport public final int rendererFormatSupport;\n\n  /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */\n  public final long timestampMs;\n\n  @Nullable private final Throwable cause;\n\n  /**\n   * Creates an instance of type {@link #TYPE_SOURCE}.\n   *\n   * @param cause The cause of the failure.\n   * @return The created instance.\n   */\n  public static ExoPlaybackException createForSource(IOException cause) {\n    return new ExoPlaybackException(TYPE_SOURCE, cause);\n  }\n\n  /**\n   * Creates an instance of type {@link #TYPE_RENDERER}.\n   *\n   * @param cause The cause of the failure.\n   * @param rendererIndex The index of the renderer in which the failure occurred.\n   * @param rendererFormat The {@link Format} the renderer was using at the time of the exception,\n   *     or null if the renderer wasn't using a {@link Format}.\n   * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code\n   *     rendererFormat}. Ignored if {@code rendererFormat} is null.\n   * @return The created instance.\n   */\n  public static ExoPlaybackException createForRenderer(\n      Exception cause,\n      int rendererIndex,\n      @Nullable Format rendererFormat,\n      @FormatSupport int rendererFormatSupport) {\n    return new ExoPlaybackException(\n        TYPE_RENDERER,\n        cause,\n        rendererIndex,\n        rendererFormat,\n        rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport);\n  }\n\n  /**\n   * Creates an instance of type {@link #TYPE_UNEXPECTED}.\n   *\n   * @param cause The cause of the failure.\n   * @return The created instance.\n   */\n  public static ExoPlaybackException createForUnexpected(RuntimeException cause) {\n    return new ExoPlaybackException(TYPE_UNEXPECTED, cause);\n  }\n\n  /**\n   * Creates an instance of type {@link #TYPE_REMOTE}.\n   *\n   * @param message The message associated with the error.\n   * @return The created instance.\n   */\n  public static ExoPlaybackException createForRemote(String message) {\n    return new ExoPlaybackException(TYPE_REMOTE, message);\n  }\n\n  /**\n   * Creates an instance of type {@link #TYPE_OUT_OF_MEMORY}.\n   *\n   * @param cause The cause of the failure.\n   * @return The created instance.\n   */\n  public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) {\n    return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause);\n  }\n\n  private ExoPlaybackException(@Type int type, Throwable cause) {\n    this(\n        type,\n        cause,\n        /* rendererIndex= */ C.INDEX_UNSET,\n        /* rendererFormat= */ null,\n        /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED);\n  }\n\n  private ExoPlaybackException(\n      @Type int type,\n      Throwable cause,\n      int rendererIndex,\n      @Nullable Format rendererFormat,\n      @FormatSupport int rendererFormatSupport) {\n    super(cause);\n    this.type = type;\n    this.cause = cause;\n    this.rendererIndex = rendererIndex;\n    this.rendererFormat = rendererFormat;\n    this.rendererFormatSupport = rendererFormatSupport;\n    timestampMs = SystemClock.elapsedRealtime();\n  }\n\n  private ExoPlaybackException(@Type int type, String message) {\n    super(message);\n    this.type = type;\n    rendererIndex = C.INDEX_UNSET;\n    rendererFormat = null;\n    rendererFormatSupport = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;\n    cause = null;\n    timestampMs = SystemClock.elapsedRealtime();\n  }\n\n  /**\n   * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.\n   *\n   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_SOURCE}.\n   */\n  public IOException getSourceException() {\n    Assertions.checkState(type == TYPE_SOURCE);\n    return (IOException) Assertions.checkNotNull(cause);\n  }\n\n  /**\n   * Retrieves the underlying error when {@link #type} is {@link #TYPE_RENDERER}.\n   *\n   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_RENDERER}.\n   */\n  public Exception getRendererException() {\n    Assertions.checkState(type == TYPE_RENDERER);\n    return (Exception) Assertions.checkNotNull(cause);\n  }\n\n  /**\n   * Retrieves the underlying error when {@link #type} is {@link #TYPE_UNEXPECTED}.\n   *\n   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_UNEXPECTED}.\n   */\n  public RuntimeException getUnexpectedException() {\n    Assertions.checkState(type == TYPE_UNEXPECTED);\n    return (RuntimeException) Assertions.checkNotNull(cause);\n  }\n\n  /**\n   * Retrieves the underlying error when {@link #type} is {@link #TYPE_OUT_OF_MEMORY}.\n   *\n   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_OUT_OF_MEMORY}.\n   */\n  public OutOfMemoryError getOutOfMemoryError() {\n    Assertions.checkState(type == TYPE_OUT_OF_MEMORY);\n    return (OutOfMemoryError) Assertions.checkNotNull(cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlayer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.content.Context;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.analytics.AnalyticsCollector;\nimport com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;\nimport com.google.android.exoplayer2.metadata.MetadataRenderer;\nimport com.google.android.exoplayer2.source.ClippingMediaSource;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\nimport com.google.android.exoplayer2.source.LoopingMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MergingMediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.SingleSampleMediaSource;\nimport com.google.android.exoplayer2.text.TextRenderer;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\n\n/**\n * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link\n * SimpleExoPlayer.Builder} or {@link Builder}.\n *\n * <h3>Player components</h3>\n *\n * <p>ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the\n * type of the media being played, how and where it is stored, and how it is rendered. Rather than\n * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this\n * work to components that are injected when a player is created or when it's prepared for playback.\n * Components common to all ExoPlayer implementations are:\n *\n * <ul>\n *   <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from\n *       which the loaded media can be read. A MediaSource is injected via {@link\n *       #prepare(MediaSource)} at the start of playback. The library modules provide default\n *       implementations for progressive media files ({@link ProgressiveMediaSource}), DASH\n *       (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an\n *       implementation for loading single media samples ({@link SingleSampleMediaSource}) that's\n *       most often used for side-loaded subtitle files, and implementations for building more\n *       complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link\n *       ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}).\n *   <li><b>{@link Renderer}</b>s that render individual components of the media. The library\n *       provides default implementations for common media types ({@link MediaCodecVideoRenderer},\n *       {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A\n *       Renderer consumes media from the MediaSource being played. Renderers are injected when the\n *       player is created.\n *   <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be\n *       consumed by each of the available Renderers. The library provides a default implementation\n *       ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected\n *       when the player is created.\n *   <li>A <b>{@link LoadControl}</b> that controls when the MediaSource buffers more media, and how\n *       much media is buffered. The library provides a default implementation ({@link\n *       DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player\n *       is created.\n * </ul>\n *\n * <p>An ExoPlayer can be built using the default components provided by the library, but may also\n * be built using custom implementations if non-standard behaviors are required. For example a\n * custom LoadControl could be injected to change the player's buffering strategy, or a custom\n * Renderer could be injected to add support for a video codec not supported natively by Android.\n *\n * <p>The concept of injecting components that implement pieces of player functionality is present\n * throughout the library. The default component implementations listed above delegate work to\n * further injected components. This allows many sub-components to be individually replaced with\n * custom implementations. For example the default MediaSource implementations require one or more\n * {@link DataSource} factories to be injected via their constructors. By providing a custom factory\n * it's possible to load data from a non-standard source, or through a different network stack.\n *\n * <h3>Threading model</h3>\n *\n * <p>The figure below shows ExoPlayer's threading model.\n *\n * <p align=\"center\"><img src=\"doc-files/exoplayer-threading-model.svg\" alt=\"ExoPlayer's threading\n * model\">\n *\n * <ul>\n *   <li>ExoPlayer instances must be accessed from a single application thread. For the vast\n *       majority of cases this should be the application's main thread. Using the application's\n *       main thread is also a requirement when using ExoPlayer's UI components or the IMA\n *       extension. The thread on which an ExoPlayer instance must be accessed can be explicitly\n *       specified by passing a `Looper` when creating the player. If no `Looper` is specified, then\n *       the `Looper` of the thread that the player is created on is used, or if that thread does\n *       not have a `Looper`, the `Looper` of the application's main thread is used. In all cases\n *       the `Looper` of the thread from which the player must be accessed can be queried using\n *       {@link #getApplicationLooper()}.\n *   <li>Registered listeners are called on the thread associated with {@link\n *       #getApplicationLooper()}. Note that this means registered listeners are called on the same\n *       thread which must be used to access the player.\n *   <li>An internal playback thread is responsible for playback. Injected player components such as\n *       Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this\n *       thread.\n *   <li>When the application performs an operation on the player, for example a seek, a message is\n *       delivered to the internal playback thread via a message queue. The internal playback thread\n *       consumes messages from the queue and performs the corresponding operations. Similarly, when\n *       a playback event occurs on the internal playback thread, a message is delivered to the\n *       application thread via a second message queue. The application thread consumes messages\n *       from the queue, updating the application visible state and calling corresponding listener\n *       methods.\n *   <li>Injected player components may use additional background threads. For example a MediaSource\n *       may use background threads to load data. These are implementation specific.\n * </ul>\n */\npublic interface ExoPlayer extends Player {\n\n  /**\n   * A builder for {@link ExoPlayer} instances.\n   *\n   * <p>See {@link #Builder(Context, Renderer...)} for the list of default values.\n   */\n  final class Builder {\n\n    private final Renderer[] renderers;\n\n    private Clock clock;\n    private TrackSelector trackSelector;\n    private LoadControl loadControl;\n    private BandwidthMeter bandwidthMeter;\n    private Looper looper;\n    private AnalyticsCollector analyticsCollector;\n    private boolean useLazyPreparation;\n    private boolean buildCalled;\n\n    /**\n     * Creates a builder with a list of {@link Renderer Renderers}.\n     *\n     * <p>The builder uses the following default values:\n     *\n     * <ul>\n     *   <li>{@link TrackSelector}: {@link DefaultTrackSelector}\n     *   <li>{@link LoadControl}: {@link DefaultLoadControl}\n     *   <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}\n     *   <li>{@link Looper}: The {@link Looper} associated with the current thread, or the {@link\n     *       Looper} of the application's main thread if the current thread doesn't have a {@link\n     *       Looper}\n     *   <li>{@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT}\n     *   <li>{@code useLazyPreparation}: {@code true}\n     *   <li>{@link Clock}: {@link Clock#DEFAULT}\n     * </ul>\n     *\n     * @param context A {@link Context}.\n     * @param renderers The {@link Renderer Renderers} to be used by the player.\n     */\n    public Builder(Context context, Renderer... renderers) {\n      this(\n          renderers,\n          new DefaultTrackSelector(context),\n          new DefaultLoadControl(),\n          DefaultBandwidthMeter.getSingletonInstance(context),\n          Util.getLooper(),\n          new AnalyticsCollector(Clock.DEFAULT),\n          /* useLazyPreparation= */ true,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * Creates a builder with the specified custom components.\n     *\n     * <p>Note that this constructor is only useful if you try to ensure that ExoPlayer's default\n     * components can be removed by ProGuard or R8. For most components except renderers, there is\n     * only a marginal benefit of doing that.\n     *\n     * @param renderers The {@link Renderer Renderers} to be used by the player.\n     * @param trackSelector A {@link TrackSelector}.\n     * @param loadControl A {@link LoadControl}.\n     * @param bandwidthMeter A {@link BandwidthMeter}.\n     * @param looper A {@link Looper} that must be used for all calls to the player.\n     * @param analyticsCollector An {@link AnalyticsCollector}.\n     * @param useLazyPreparation Whether media sources should be initialized lazily.\n     * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}.\n     */\n    public Builder(\n        Renderer[] renderers,\n        TrackSelector trackSelector,\n        LoadControl loadControl,\n        BandwidthMeter bandwidthMeter,\n        Looper looper,\n        AnalyticsCollector analyticsCollector,\n        boolean useLazyPreparation,\n        Clock clock) {\n      Assertions.checkArgument(renderers.length > 0);\n      this.renderers = renderers;\n      this.trackSelector = trackSelector;\n      this.loadControl = loadControl;\n      this.bandwidthMeter = bandwidthMeter;\n      this.looper = looper;\n      this.analyticsCollector = analyticsCollector;\n      this.useLazyPreparation = useLazyPreparation;\n      this.clock = clock;\n    }\n\n    /**\n     * Sets the {@link TrackSelector} that will be used by the player.\n     *\n     * @param trackSelector A {@link TrackSelector}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setTrackSelector(TrackSelector trackSelector) {\n      Assertions.checkState(!buildCalled);\n      this.trackSelector = trackSelector;\n      return this;\n    }\n\n    /**\n     * Sets the {@link LoadControl} that will be used by the player.\n     *\n     * @param loadControl A {@link LoadControl}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setLoadControl(LoadControl loadControl) {\n      Assertions.checkState(!buildCalled);\n      this.loadControl = loadControl;\n      return this;\n    }\n\n    /**\n     * Sets the {@link BandwidthMeter} that will be used by the player.\n     *\n     * @param bandwidthMeter A {@link BandwidthMeter}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) {\n      Assertions.checkState(!buildCalled);\n      this.bandwidthMeter = bandwidthMeter;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Looper} that must be used for all calls to the player and that is used to\n     * call listeners on.\n     *\n     * @param looper A {@link Looper}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setLooper(Looper looper) {\n      Assertions.checkState(!buildCalled);\n      this.looper = looper;\n      return this;\n    }\n\n    /**\n     * Sets the {@link AnalyticsCollector} that will collect and forward all player events.\n     *\n     * @param analyticsCollector An {@link AnalyticsCollector}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) {\n      Assertions.checkState(!buildCalled);\n      this.analyticsCollector = analyticsCollector;\n      return this;\n    }\n\n    /**\n     * Sets whether media sources should be initialized lazily.\n     *\n     * <p>If false, all initial preparation steps (e.g., manifest loads) happen immediately. If\n     * true, these initial preparations are triggered only when the player starts buffering the\n     * media.\n     *\n     * @param useLazyPreparation Whether to use lazy preparation.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setUseLazyPreparation(boolean useLazyPreparation) {\n      Assertions.checkState(!buildCalled);\n      this.useLazyPreparation = useLazyPreparation;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Clock} that will be used by the player. Should only be set for testing\n     * purposes.\n     *\n     * @param clock A {@link Clock}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    @VisibleForTesting\n    public Builder setClock(Clock clock) {\n      Assertions.checkState(!buildCalled);\n      this.clock = clock;\n      return this;\n    }\n\n    /**\n     * Builds an {@link ExoPlayer} instance.\n     *\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public ExoPlayer build() {\n      Assertions.checkState(!buildCalled);\n      buildCalled = true;\n      return new ExoPlayerImpl(\n          renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);\n    }\n  }\n\n  /** Returns the {@link Looper} associated with the playback thread. */\n  Looper getPlaybackLooper();\n\n  /**\n   * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback\n   * has not failed or been stopped.\n   */\n  void retry();\n\n  /**\n   * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code\n   * prepare(mediaSource, true, true)}.\n   */\n  void prepare(MediaSource mediaSource);\n\n  /**\n   * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback\n   * position the default position in the first {@link Timeline.Window}.\n   *\n   * @param mediaSource The {@link MediaSource} to play.\n   * @param resetPosition Whether the playback position should be reset to the default position in\n   *     the first {@link Timeline.Window}. If false, playback will start from the position defined\n   *     by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.\n   * @param resetState Whether the timeline, manifest, tracks and track selections should be reset.\n   *     Should be true unless the player is being prepared to play the same media as it was playing\n   *     previously (e.g. if playback failed and is being retried).\n   */\n  void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);\n\n  /**\n   * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message\n   * will be delivered immediately without blocking on the playback thread. The default {@link\n   * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a\n   * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be\n   * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}.\n   * Alternatively, the message can be sent at a specific window using {@link\n   * PlayerMessage#setPosition(int, long)}.\n   */\n  PlayerMessage createMessage(PlayerMessage.Target target);\n\n  /**\n   * Sets the parameters that control how seek operations are performed.\n   *\n   * @param seekParameters The seek parameters, or {@code null} to use the defaults.\n   */\n  void setSeekParameters(@Nullable SeekParameters seekParameters);\n\n  /** Returns the currently active {@link SeekParameters} of the player. */\n  SeekParameters getSeekParameters();\n\n  /**\n   * Sets whether the player is allowed to keep holding limited resources such as video decoders,\n   * even when in the idle state. By doing so, the player may be able to reduce latency when\n   * starting to play another piece of content for which the same resources are required.\n   *\n   * <p>This mode should be used with caution, since holding limited resources may prevent other\n   * players of media components from acquiring them. It should only be enabled when <em>both</em>\n   * of the following conditions are true:\n   *\n   * <ul>\n   *   <li>The application that owns the player is in the foreground.\n   *   <li>The player is used in a way that may benefit from foreground mode. For this to be true,\n   *       the same player instance must be used to play multiple pieces of content, and there must\n   *       be gaps between the playbacks (i.e. {@link #stop} is called to halt one playback, and\n   *       {@link #prepare} is called some time later to start a new one).\n   * </ul>\n   *\n   * <p>Note that foreground mode is <em>not</em> useful for switching between content without gaps\n   * between the playbacks. For this use case {@link #stop} does not need to be called, and simply\n   * calling {@link #prepare} for the new media will cause limited resources to be retained even if\n   * foreground mode is not enabled.\n   *\n   * <p>If foreground mode is enabled, it's the application's responsibility to disable it when the\n   * conditions described above no longer hold.\n   *\n   * @param foregroundMode Whether the player is allowed to keep limited resources even when in the\n   *     idle state.\n   */\n  void setForegroundMode(boolean foregroundMode);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.content.Context;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.analytics.AnalyticsCollector;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Util;\n\n/** @deprecated Use {@link SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder} instead. */\n@Deprecated\npublic final class ExoPlayerFactory {\n\n  private ExoPlayerFactory() {}\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {\n    RenderersFactory renderersFactory =\n        new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);\n    return newSimpleInstance(\n        context, renderersFactory, trackSelector, loadControl, drmSessionManager);\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,\n      long allowedVideoJoiningTimeMs) {\n    RenderersFactory renderersFactory =\n        new DefaultRenderersFactory(context)\n            .setExtensionRendererMode(extensionRendererMode)\n            .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);\n    return newSimpleInstance(\n        context, renderersFactory, trackSelector, loadControl, drmSessionManager);\n  }\n\n  /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(Context context) {\n    return newSimpleInstance(context, new DefaultTrackSelector(context));\n  }\n\n  /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {\n    return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);\n  }\n\n  /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) {\n    return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());\n  }\n\n  /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context, TrackSelector trackSelector, LoadControl loadControl) {\n    RenderersFactory renderersFactory = new DefaultRenderersFactory(context);\n    return newSimpleInstance(context, renderersFactory, trackSelector, loadControl);\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    RenderersFactory renderersFactory = new DefaultRenderersFactory(context);\n    return newSimpleInstance(\n        context, renderersFactory, trackSelector, loadControl, drmSessionManager);\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    return newSimpleInstance(\n        context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);\n  }\n\n  /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        /* drmSessionManager= */ null,\n        Util.getLooper());\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    return newSimpleInstance(\n        context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper());\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      BandwidthMeter bandwidthMeter) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        bandwidthMeter,\n        new AnalyticsCollector(Clock.DEFAULT),\n        Util.getLooper());\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      AnalyticsCollector analyticsCollector) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        analyticsCollector,\n        Util.getLooper());\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      Looper looper) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        new AnalyticsCollector(Clock.DEFAULT),\n        looper);\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      AnalyticsCollector analyticsCollector,\n      Looper looper) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        DefaultBandwidthMeter.getSingletonInstance(context),\n        analyticsCollector,\n        looper);\n  }\n\n  /**\n   * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot\n   *     be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link\n   *     MediaSource} factories.\n   */\n  @SuppressWarnings(\"deprecation\")\n  @Deprecated\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      BandwidthMeter bandwidthMeter,\n      AnalyticsCollector analyticsCollector,\n      Looper looper) {\n    return new SimpleExoPlayer(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        bandwidthMeter,\n        analyticsCollector,\n        Clock.DEFAULT,\n        looper);\n  }\n\n  /** @deprecated Use {@link ExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static ExoPlayer newInstance(\n      Context context, Renderer[] renderers, TrackSelector trackSelector) {\n    return newInstance(context, renderers, trackSelector, new DefaultLoadControl());\n  }\n\n  /** @deprecated Use {@link ExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static ExoPlayer newInstance(\n      Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {\n    return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper());\n  }\n\n  /** @deprecated Use {@link ExoPlayer.Builder} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static ExoPlayer newInstance(\n      Context context,\n      Renderer[] renderers,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      Looper looper) {\n    return newInstance(\n        context,\n        renderers,\n        trackSelector,\n        loadControl,\n        DefaultBandwidthMeter.getSingletonInstance(context),\n        looper);\n  }\n\n  /** @deprecated Use {@link ExoPlayer.Builder} instead. */\n  @Deprecated\n  public static ExoPlayer newInstance(\n      Context context,\n      Renderer[] renderers,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      Looper looper) {\n    return new ExoPlayerImpl(\n        renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.annotation.SuppressLint;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayDeque;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * An {@link ExoPlayer} implementation. Instances can be obtained from {@link Builder}.\n */\n/* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer {\n\n  private static final String TAG = \"ExoPlayerImpl\";\n\n  /**\n   * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}\n   * when the player does not have any track selection made (such as when player is reset, or when\n   * player seeks to an unprepared period). It will not be used as result of any {@link\n   * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}\n   * operation.\n   */\n  /* package */ final TrackSelectorResult emptyTrackSelectorResult;\n\n  private final Renderer[] renderers;\n  private final TrackSelector trackSelector;\n  private final Handler eventHandler;\n  private final ExoPlayerImplInternal internalPlayer;\n  private final Handler internalPlayerHandler;\n  private final CopyOnWriteArrayList<ListenerHolder> listeners;\n  private final Timeline.Period period;\n  private final ArrayDeque<Runnable> pendingListenerNotifications;\n\n  private MediaSource mediaSource;\n  private boolean playWhenReady;\n  @PlaybackSuppressionReason private int playbackSuppressionReason;\n  @RepeatMode private int repeatMode;\n  private boolean shuffleModeEnabled;\n  private int pendingOperationAcks;\n  private boolean hasPendingPrepare;\n  private boolean hasPendingSeek;\n  private boolean foregroundMode;\n  private int pendingSetPlaybackParametersAcks;\n  private PlaybackParameters playbackParameters;\n  private SeekParameters seekParameters;\n\n  // Playback information when there is no pending seek/set source operation.\n  private PlaybackInfo playbackInfo;\n\n  // Playback information when there is a pending seek/set source operation.\n  private int maskingWindowIndex;\n  private int maskingPeriodIndex;\n  private long maskingWindowPositionMs;\n\n  /**\n   * Constructs an instance. Must be called from a thread that has an associated {@link Looper}.\n   *\n   * @param renderers The {@link Renderer}s that will be used by the instance.\n   * @param trackSelector The {@link TrackSelector} that will be used by the instance.\n   * @param loadControl The {@link LoadControl} that will be used by the instance.\n   * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.\n   * @param clock The {@link Clock} that will be used by the instance.\n   * @param looper The {@link Looper} which must be used for all calls to the player and which is\n   *     used to call listeners on.\n   */\n  @SuppressLint(\"HandlerLeak\")\n  public ExoPlayerImpl(\n      Renderer[] renderers,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      Clock clock,\n      Looper looper) {\n    Log.i(TAG, \"Init \" + Integer.toHexString(System.identityHashCode(this)) + \" [\"\n        + ExoPlayerLibraryInfo.VERSION_SLASHY + \"] [\" + Util.DEVICE_DEBUG_INFO + \"]\");\n    Assertions.checkState(renderers.length > 0);\n    this.renderers = Assertions.checkNotNull(renderers);\n    this.trackSelector = Assertions.checkNotNull(trackSelector);\n    this.playWhenReady = false;\n    this.repeatMode = Player.REPEAT_MODE_OFF;\n    this.shuffleModeEnabled = false;\n    this.listeners = new CopyOnWriteArrayList<>();\n    emptyTrackSelectorResult =\n        new TrackSelectorResult(\n            new RendererConfiguration[renderers.length],\n            new TrackSelection[renderers.length],\n            null);\n    period = new Timeline.Period();\n    playbackParameters = PlaybackParameters.DEFAULT;\n    seekParameters = SeekParameters.DEFAULT;\n    playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;\n    eventHandler =\n        new Handler(looper) {\n          @Override\n          public void handleMessage(Message msg) {\n            ExoPlayerImpl.this.handleEvent(msg);\n          }\n        };\n    playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);\n    pendingListenerNotifications = new ArrayDeque<>();\n    internalPlayer =\n        new ExoPlayerImplInternal(\n            renderers,\n            trackSelector,\n            emptyTrackSelectorResult,\n            loadControl,\n            bandwidthMeter,\n            playWhenReady,\n            repeatMode,\n            shuffleModeEnabled,\n            eventHandler,\n            clock);\n    internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());\n  }\n\n  @Override\n  @Nullable\n  public AudioComponent getAudioComponent() {\n    return null;\n  }\n\n  @Override\n  @Nullable\n  public VideoComponent getVideoComponent() {\n    return null;\n  }\n\n  @Override\n  @Nullable\n  public TextComponent getTextComponent() {\n    return null;\n  }\n\n  @Override\n  @Nullable\n  public MetadataComponent getMetadataComponent() {\n    return null;\n  }\n\n  @Override\n  public Looper getPlaybackLooper() {\n    return internalPlayer.getPlaybackLooper();\n  }\n\n  @Override\n  public Looper getApplicationLooper() {\n    return eventHandler.getLooper();\n  }\n\n  @Override\n  public void addListener(EventListener listener) {\n    listeners.addIfAbsent(new ListenerHolder(listener));\n  }\n\n  @Override\n  public void removeListener(EventListener listener) {\n    for (ListenerHolder listenerHolder : listeners) {\n      if (listenerHolder.listener.equals(listener)) {\n        listenerHolder.release();\n        listeners.remove(listenerHolder);\n      }\n    }\n  }\n\n  @Override\n  @State\n  public int getPlaybackState() {\n    return playbackInfo.playbackState;\n  }\n\n  @Override\n  @PlaybackSuppressionReason\n  public int getPlaybackSuppressionReason() {\n    return playbackSuppressionReason;\n  }\n\n  @Override\n  @Nullable\n  public ExoPlaybackException getPlaybackError() {\n    return playbackInfo.playbackError;\n  }\n\n  @Override\n  public void retry() {\n    if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) {\n      prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);\n    }\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource) {\n    prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {\n    this.mediaSource = mediaSource;\n    PlaybackInfo playbackInfo =\n        getResetPlaybackInfo(\n            resetPosition,\n            resetState,\n            /* resetError= */ true,\n            /* playbackState= */ Player.STATE_BUFFERING);\n    // Trigger internal prepare first before updating the playback info and notifying external\n    // listeners to ensure that new operations issued in the listener notifications reach the\n    // player after this prepare. The internal player can't change the playback info immediately\n    // because it uses a callback.\n    hasPendingPrepare = true;\n    pendingOperationAcks++;\n    internalPlayer.prepare(mediaSource, resetPosition, resetState);\n    updatePlaybackInfo(\n        playbackInfo,\n        /* positionDiscontinuity= */ false,\n        /* ignored */ DISCONTINUITY_REASON_INTERNAL,\n        TIMELINE_CHANGE_REASON_RESET,\n        /* seekProcessed= */ false);\n  }\n\n\n  @Override\n  public void setPlayWhenReady(boolean playWhenReady) {\n    setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE);\n  }\n\n  public void setPlayWhenReady(\n      boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {\n    boolean oldIsPlaying = isPlaying();\n    boolean oldInternalPlayWhenReady =\n        this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;\n    boolean internalPlayWhenReady =\n        playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;\n    if (oldInternalPlayWhenReady != internalPlayWhenReady) {\n      internalPlayer.setPlayWhenReady(internalPlayWhenReady);\n    }\n    boolean playWhenReadyChanged = this.playWhenReady != playWhenReady;\n    boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason;\n    this.playWhenReady = playWhenReady;\n    this.playbackSuppressionReason = playbackSuppressionReason;\n    boolean isPlaying = isPlaying();\n    boolean isPlayingChanged = oldIsPlaying != isPlaying;\n    if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) {\n      int playbackState = playbackInfo.playbackState;\n      notifyListeners(\n          listener -> {\n            if (playWhenReadyChanged) {\n              listener.onPlayerStateChanged(playWhenReady, playbackState);\n            }\n            if (suppressionReasonChanged) {\n              listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);\n            }\n            if (isPlayingChanged) {\n              listener.onIsPlayingChanged(isPlaying);\n            }\n          });\n    }\n  }\n\n  @Override\n  public boolean getPlayWhenReady() {\n    return playWhenReady;\n  }\n\n  @Override\n  public void setRepeatMode(@RepeatMode int repeatMode) {\n    if (this.repeatMode != repeatMode) {\n      this.repeatMode = repeatMode;\n      internalPlayer.setRepeatMode(repeatMode);\n      notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode));\n    }\n  }\n\n  @Override\n  public @RepeatMode int getRepeatMode() {\n    return repeatMode;\n  }\n\n  @Override\n  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {\n    if (this.shuffleModeEnabled != shuffleModeEnabled) {\n      this.shuffleModeEnabled = shuffleModeEnabled;\n      internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);\n      notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));\n    }\n  }\n\n  @Override\n  public boolean getShuffleModeEnabled() {\n    return shuffleModeEnabled;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return playbackInfo.isLoading;\n  }\n\n  @Override\n  public void seekTo(int windowIndex, long positionMs) {\n    Timeline timeline = playbackInfo.timeline;\n    if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {\n      throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);\n    }\n    hasPendingSeek = true;\n    pendingOperationAcks++;\n    if (isPlayingAd()) {\n      // TODO: Investigate adding support for seeking during ads. This is complicated to do in\n      // general because the midroll ad preceding the seek destination must be played before the\n      // content position can be played, if a different ad is playing at the moment.\n      Log.w(TAG, \"seekTo ignored because an ad is playing\");\n      eventHandler\n          .obtainMessage(\n              ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED,\n              /* operationAcks */ 1,\n              /* positionDiscontinuityReason */ C.INDEX_UNSET,\n              playbackInfo)\n          .sendToTarget();\n      return;\n    }\n    maskingWindowIndex = windowIndex;\n    if (timeline.isEmpty()) {\n      maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs;\n      maskingPeriodIndex = 0;\n    } else {\n      long windowPositionUs = positionMs == C.TIME_UNSET\n          ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);\n      Pair<Object, Long> periodUidAndPosition =\n          timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);\n      maskingWindowPositionMs = C.usToMs(windowPositionUs);\n      maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);\n    }\n    internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));\n    notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));\n  }\n\n  @Override\n  public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {\n    if (playbackParameters == null) {\n      playbackParameters = PlaybackParameters.DEFAULT;\n    }\n    if (this.playbackParameters.equals(playbackParameters)) {\n      return;\n    }\n    pendingSetPlaybackParametersAcks++;\n    this.playbackParameters = playbackParameters;\n    internalPlayer.setPlaybackParameters(playbackParameters);\n    PlaybackParameters playbackParametersToNotify = playbackParameters;\n    notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify));\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return playbackParameters;\n  }\n\n  @Override\n  public void setSeekParameters(@Nullable SeekParameters seekParameters) {\n    if (seekParameters == null) {\n      seekParameters = SeekParameters.DEFAULT;\n    }\n    if (!this.seekParameters.equals(seekParameters)) {\n      this.seekParameters = seekParameters;\n      internalPlayer.setSeekParameters(seekParameters);\n    }\n  }\n\n  @Override\n  public SeekParameters getSeekParameters() {\n    return seekParameters;\n  }\n\n  @Override\n  public void setForegroundMode(boolean foregroundMode) {\n    if (this.foregroundMode != foregroundMode) {\n      this.foregroundMode = foregroundMode;\n      internalPlayer.setForegroundMode(foregroundMode);\n    }\n  }\n\n  @Override\n  public void stop(boolean reset) {\n    if (reset) {\n      mediaSource = null;\n    }\n    PlaybackInfo playbackInfo =\n        getResetPlaybackInfo(\n            /* resetPosition= */ reset,\n            /* resetState= */ reset,\n            /* resetError= */ reset,\n            /* playbackState= */ Player.STATE_IDLE);\n    // Trigger internal stop first before updating the playback info and notifying external\n    // listeners to ensure that new operations issued in the listener notifications reach the\n    // player after this stop. The internal player can't change the playback info immediately\n    // because it uses a callback.\n    pendingOperationAcks++;\n    internalPlayer.stop(reset);\n    updatePlaybackInfo(\n        playbackInfo,\n        /* positionDiscontinuity= */ false,\n        /* ignored */ DISCONTINUITY_REASON_INTERNAL,\n        TIMELINE_CHANGE_REASON_RESET,\n        /* seekProcessed= */ false);\n  }\n\n  @Override\n  public void release() {\n    Log.i(TAG, \"Release \" + Integer.toHexString(System.identityHashCode(this)) + \" [\"\n        + ExoPlayerLibraryInfo.VERSION_SLASHY + \"] [\" + Util.DEVICE_DEBUG_INFO + \"] [\"\n        + ExoPlayerLibraryInfo.registeredModules() + \"]\");\n    mediaSource = null;\n    internalPlayer.release();\n    eventHandler.removeCallbacksAndMessages(null);\n    playbackInfo =\n        getResetPlaybackInfo(\n            /* resetPosition= */ false,\n            /* resetState= */ false,\n            /* resetError= */ false,\n            /* playbackState= */ Player.STATE_IDLE);\n  }\n\n  @Override\n  public PlayerMessage createMessage(Target target) {\n    return new PlayerMessage(\n        internalPlayer,\n        target,\n        playbackInfo.timeline,\n        getCurrentWindowIndex(),\n        internalPlayerHandler);\n  }\n\n  @Override\n  public int getCurrentPeriodIndex() {\n    if (shouldMaskPosition()) {\n      return maskingPeriodIndex;\n    } else {\n      return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);\n    }\n  }\n\n  @Override\n  public int getCurrentWindowIndex() {\n    if (shouldMaskPosition()) {\n      return maskingWindowIndex;\n    } else {\n      return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)\n          .windowIndex;\n    }\n  }\n\n  @Override\n  public long getDuration() {\n    if (isPlayingAd()) {\n      MediaPeriodId periodId = playbackInfo.periodId;\n      playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);\n      long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup);\n      return C.usToMs(adDurationUs);\n    }\n    return getContentDuration();\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    if (shouldMaskPosition()) {\n      return maskingWindowPositionMs;\n    } else if (playbackInfo.periodId.isAd()) {\n      return C.usToMs(playbackInfo.positionUs);\n    } else {\n      return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs);\n    }\n  }\n\n  @Override\n  public long getBufferedPosition() {\n    if (isPlayingAd()) {\n      return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)\n          ? C.usToMs(playbackInfo.bufferedPositionUs)\n          : getDuration();\n    }\n    return getContentBufferedPosition();\n  }\n\n  @Override\n  public long getTotalBufferedDuration() {\n    return C.usToMs(playbackInfo.totalBufferedDurationUs);\n  }\n\n  @Override\n  public boolean isPlayingAd() {\n    return !shouldMaskPosition() && playbackInfo.periodId.isAd();\n  }\n\n  @Override\n  public int getCurrentAdGroupIndex() {\n    return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getCurrentAdIndexInAdGroup() {\n    return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;\n  }\n\n  @Override\n  public long getContentPosition() {\n    if (isPlayingAd()) {\n      playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);\n      return playbackInfo.contentPositionUs == C.TIME_UNSET\n          ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()\n          : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);\n    } else {\n      return getCurrentPosition();\n    }\n  }\n\n  @Override\n  public long getContentBufferedPosition() {\n    if (shouldMaskPosition()) {\n      return maskingWindowPositionMs;\n    }\n    if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber\n        != playbackInfo.periodId.windowSequenceNumber) {\n      return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();\n    }\n    long contentBufferedPositionUs = playbackInfo.bufferedPositionUs;\n    if (playbackInfo.loadingMediaPeriodId.isAd()) {\n      Timeline.Period loadingPeriod =\n          playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period);\n      contentBufferedPositionUs =\n          loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex);\n      if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) {\n        contentBufferedPositionUs = loadingPeriod.durationUs;\n      }\n    }\n    return periodPositionUsToWindowPositionMs(\n        playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs);\n  }\n\n  @Override\n  public int getRendererCount() {\n    return renderers.length;\n  }\n\n  @Override\n  public int getRendererType(int index) {\n    return renderers[index].getTrackType();\n  }\n\n  @Override\n  public TrackGroupArray getCurrentTrackGroups() {\n    return playbackInfo.trackGroups;\n  }\n\n  @Override\n  public TrackSelectionArray getCurrentTrackSelections() {\n    return playbackInfo.trackSelectorResult.selections;\n  }\n\n  @Override\n  public Timeline getCurrentTimeline() {\n    return playbackInfo.timeline;\n  }\n\n  // Not private so it can be called from an inner class without going through a thunk method.\n  /* package */ void handleEvent(Message msg) {\n    switch (msg.what) {\n      case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:\n        handlePlaybackInfo(\n            (PlaybackInfo) msg.obj,\n            /* operationAcks= */ msg.arg1,\n            /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,\n            /* positionDiscontinuityReason= */ msg.arg2);\n        break;\n      case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:\n        handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0);\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  private void handlePlaybackParameters(\n      PlaybackParameters playbackParameters, boolean operationAck) {\n    if (operationAck) {\n      pendingSetPlaybackParametersAcks--;\n    }\n    if (pendingSetPlaybackParametersAcks == 0) {\n      if (!this.playbackParameters.equals(playbackParameters)) {\n        this.playbackParameters = playbackParameters;\n        notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));\n      }\n    }\n  }\n\n  private void handlePlaybackInfo(\n      PlaybackInfo playbackInfo,\n      int operationAcks,\n      boolean positionDiscontinuity,\n      @DiscontinuityReason int positionDiscontinuityReason) {\n    pendingOperationAcks -= operationAcks;\n    if (pendingOperationAcks == 0) {\n      if (playbackInfo.startPositionUs == C.TIME_UNSET) {\n        // Replace internal unset start position with externally visible start position of zero.\n        playbackInfo =\n            playbackInfo.copyWithNewPosition(\n                playbackInfo.periodId,\n                /* positionUs= */ 0,\n                playbackInfo.contentPositionUs,\n                playbackInfo.totalBufferedDurationUs);\n      }\n      if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {\n        // Update the masking variables, which are used when the timeline becomes empty.\n        maskingPeriodIndex = 0;\n        maskingWindowIndex = 0;\n        maskingWindowPositionMs = 0;\n      }\n      @TimelineChangeReason\n      int timelineChangeReason =\n          hasPendingPrepare\n              ? Player.TIMELINE_CHANGE_REASON_PREPARED\n              : Player.TIMELINE_CHANGE_REASON_DYNAMIC;\n      boolean seekProcessed = hasPendingSeek;\n      hasPendingPrepare = false;\n      hasPendingSeek = false;\n      updatePlaybackInfo(\n          playbackInfo,\n          positionDiscontinuity,\n          positionDiscontinuityReason,\n          timelineChangeReason,\n          seekProcessed);\n    }\n  }\n\n  private PlaybackInfo getResetPlaybackInfo(\n      boolean resetPosition,\n      boolean resetState,\n      boolean resetError,\n      @State int playbackState) {\n    if (resetPosition) {\n      maskingWindowIndex = 0;\n      maskingPeriodIndex = 0;\n      maskingWindowPositionMs = 0;\n    } else {\n      maskingWindowIndex = getCurrentWindowIndex();\n      maskingPeriodIndex = getCurrentPeriodIndex();\n      maskingWindowPositionMs = getCurrentPosition();\n    }\n    // Also reset period-based PlaybackInfo positions if resetting the state.\n    resetPosition = resetPosition || resetState;\n    MediaPeriodId mediaPeriodId =\n        resetPosition\n            ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)\n            : playbackInfo.periodId;\n    long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;\n    long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;\n    return new PlaybackInfo(\n        resetState ? Timeline.EMPTY : playbackInfo.timeline,\n        mediaPeriodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        resetError ? null : playbackInfo.playbackError,\n        /* isLoading= */ false,\n        resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,\n        resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,\n        mediaPeriodId,\n        startPositionUs,\n        /* totalBufferedDurationUs= */ 0,\n        startPositionUs);\n  }\n\n  private void updatePlaybackInfo(\n      PlaybackInfo playbackInfo,\n      boolean positionDiscontinuity,\n      @DiscontinuityReason int positionDiscontinuityReason,\n      @TimelineChangeReason int timelineChangeReason,\n      boolean seekProcessed) {\n    boolean previousIsPlaying = isPlaying();\n    // Assign playback info immediately such that all getters return the right values.\n    PlaybackInfo previousPlaybackInfo = this.playbackInfo;\n    this.playbackInfo = playbackInfo;\n    boolean isPlaying = isPlaying();\n    notifyListeners(\n        new PlaybackInfoUpdate(\n            playbackInfo,\n            previousPlaybackInfo,\n            listeners,\n            trackSelector,\n            positionDiscontinuity,\n            positionDiscontinuityReason,\n            timelineChangeReason,\n            seekProcessed,\n            playWhenReady,\n            /* isPlayingChanged= */ previousIsPlaying != isPlaying));\n  }\n\n  private void notifyListeners(ListenerInvocation listenerInvocation) {\n    CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);\n    notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));\n  }\n\n  private void notifyListeners(Runnable listenerNotificationRunnable) {\n    boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty();\n    pendingListenerNotifications.addLast(listenerNotificationRunnable);\n    if (isRunningRecursiveListenerNotification) {\n      return;\n    }\n    while (!pendingListenerNotifications.isEmpty()) {\n      pendingListenerNotifications.peekFirst().run();\n      pendingListenerNotifications.removeFirst();\n    }\n  }\n\n  private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) {\n    long positionMs = C.usToMs(positionUs);\n    playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);\n    positionMs += period.getPositionInWindowMs();\n    return positionMs;\n  }\n\n  private boolean shouldMaskPosition() {\n    return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;\n  }\n\n  private static final class PlaybackInfoUpdate implements Runnable {\n\n    private final PlaybackInfo playbackInfo;\n    private final CopyOnWriteArrayList<ListenerHolder> listenerSnapshot;\n    private final TrackSelector trackSelector;\n    private final boolean positionDiscontinuity;\n    private final @DiscontinuityReason int positionDiscontinuityReason;\n    private final @TimelineChangeReason int timelineChangeReason;\n    private final boolean seekProcessed;\n    private final boolean playbackStateChanged;\n    private final boolean playbackErrorChanged;\n    private final boolean timelineChanged;\n    private final boolean isLoadingChanged;\n    private final boolean trackSelectorResultChanged;\n    private final boolean playWhenReady;\n    private final boolean isPlayingChanged;\n\n    public PlaybackInfoUpdate(\n        PlaybackInfo playbackInfo,\n        PlaybackInfo previousPlaybackInfo,\n        CopyOnWriteArrayList<ListenerHolder> listeners,\n        TrackSelector trackSelector,\n        boolean positionDiscontinuity,\n        @DiscontinuityReason int positionDiscontinuityReason,\n        @TimelineChangeReason int timelineChangeReason,\n        boolean seekProcessed,\n        boolean playWhenReady,\n        boolean isPlayingChanged) {\n      this.playbackInfo = playbackInfo;\n      this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);\n      this.trackSelector = trackSelector;\n      this.positionDiscontinuity = positionDiscontinuity;\n      this.positionDiscontinuityReason = positionDiscontinuityReason;\n      this.timelineChangeReason = timelineChangeReason;\n      this.seekProcessed = seekProcessed;\n      this.playWhenReady = playWhenReady;\n      this.isPlayingChanged = isPlayingChanged;\n      playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;\n      playbackErrorChanged =\n          previousPlaybackInfo.playbackError != playbackInfo.playbackError\n              && playbackInfo.playbackError != null;\n      timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;\n      isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;\n      trackSelectorResultChanged =\n          previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;\n    }\n\n    @Override\n    public void run() {\n      if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {\n        invokeAll(\n            listenerSnapshot,\n            listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));\n      }\n      if (positionDiscontinuity) {\n        invokeAll(\n            listenerSnapshot,\n            listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));\n      }\n      if (playbackErrorChanged) {\n        invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError));\n      }\n      if (trackSelectorResultChanged) {\n        trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);\n        invokeAll(\n            listenerSnapshot,\n            listener ->\n                listener.onTracksChanged(\n                    playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections));\n      }\n      if (isLoadingChanged) {\n        invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading));\n      }\n      if (playbackStateChanged) {\n        invokeAll(\n            listenerSnapshot,\n            listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState));\n      }\n      if (isPlayingChanged) {\n        invokeAll(\n            listenerSnapshot,\n            listener ->\n                listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY));\n      }\n      if (seekProcessed) {\n        invokeAll(listenerSnapshot, EventListener::onSeekProcessed);\n      }\n    }\n  }\n\n  private static void invokeAll(\n      CopyOnWriteArrayList<ListenerHolder> listeners, ListenerInvocation listenerInvocation) {\n    for (ListenerHolder listenerHolder : listeners) {\n      listenerHolder.invoke(listenerInvocation);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.os.Process;\nimport android.os.SystemClock;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/** Implements the internal behavior of {@link ExoPlayerImpl}. */\n/* package */ final class ExoPlayerImplInternal\n    implements Handler.Callback,\n        MediaPeriod.Callback,\n        TrackSelector.InvalidationListener,\n        MediaSourceCaller,\n        PlaybackParameterListener,\n        PlayerMessage.Sender {\n\n  private static final String TAG = \"ExoPlayerImplInternal\";\n\n  // External messages\n  public static final int MSG_PLAYBACK_INFO_CHANGED = 0;\n  public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1;\n\n  // Internal messages\n  private static final int MSG_PREPARE = 0;\n  private static final int MSG_SET_PLAY_WHEN_READY = 1;\n  private static final int MSG_DO_SOME_WORK = 2;\n  private static final int MSG_SEEK_TO = 3;\n  private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;\n  private static final int MSG_SET_SEEK_PARAMETERS = 5;\n  private static final int MSG_STOP = 6;\n  private static final int MSG_RELEASE = 7;\n  private static final int MSG_REFRESH_SOURCE_INFO = 8;\n  private static final int MSG_PERIOD_PREPARED = 9;\n  private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;\n  private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;\n  private static final int MSG_SET_REPEAT_MODE = 12;\n  private static final int MSG_SET_SHUFFLE_ENABLED = 13;\n  private static final int MSG_SET_FOREGROUND_MODE = 14;\n  private static final int MSG_SEND_MESSAGE = 15;\n  private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;\n  private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;\n\n  private static final int ACTIVE_INTERVAL_MS = 10;\n  private static final int IDLE_INTERVAL_MS = 1000;\n\n  private final Renderer[] renderers;\n  private final RendererCapabilities[] rendererCapabilities;\n  private final TrackSelector trackSelector;\n  private final TrackSelectorResult emptyTrackSelectorResult;\n  private final LoadControl loadControl;\n  private final BandwidthMeter bandwidthMeter;\n  private final HandlerWrapper handler;\n  private final HandlerThread internalPlaybackThread;\n  private final Handler eventHandler;\n  private final Timeline.Window window;\n  private final Timeline.Period period;\n  private final long backBufferDurationUs;\n  private final boolean retainBackBufferFromKeyframe;\n  private final DefaultMediaClock mediaClock;\n  private final PlaybackInfoUpdate playbackInfoUpdate;\n  private final ArrayList<PendingMessageInfo> pendingMessages;\n  private final Clock clock;\n  private final MediaPeriodQueue queue;\n\n  @SuppressWarnings(\"unused\")\n  private SeekParameters seekParameters;\n\n  private PlaybackInfo playbackInfo;\n  private MediaSource mediaSource;\n  private Renderer[] enabledRenderers;\n  private boolean released;\n  private boolean playWhenReady;\n  private boolean rebuffering;\n  private boolean shouldContinueLoading;\n  @Player.RepeatMode private int repeatMode;\n  private boolean shuffleModeEnabled;\n  private boolean foregroundMode;\n\n  private int pendingPrepareCount;\n  private SeekPosition pendingInitialSeekPosition;\n  private long rendererPositionUs;\n  private int nextPendingMessageIndex;\n  private boolean deliverPendingMessageAtStartPositionRequired;\n\n  public ExoPlayerImplInternal(\n      Renderer[] renderers,\n      TrackSelector trackSelector,\n      TrackSelectorResult emptyTrackSelectorResult,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      boolean playWhenReady,\n      @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled,\n      Handler eventHandler,\n      Clock clock) {\n    this.renderers = renderers;\n    this.trackSelector = trackSelector;\n    this.emptyTrackSelectorResult = emptyTrackSelectorResult;\n    this.loadControl = loadControl;\n    this.bandwidthMeter = bandwidthMeter;\n    this.playWhenReady = playWhenReady;\n    this.repeatMode = repeatMode;\n    this.shuffleModeEnabled = shuffleModeEnabled;\n    this.eventHandler = eventHandler;\n    this.clock = clock;\n    this.queue = new MediaPeriodQueue();\n\n    backBufferDurationUs = loadControl.getBackBufferDurationUs();\n    retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe();\n\n    seekParameters = SeekParameters.DEFAULT;\n    playbackInfo =\n        PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);\n    playbackInfoUpdate = new PlaybackInfoUpdate();\n    rendererCapabilities = new RendererCapabilities[renderers.length];\n    for (int i = 0; i < renderers.length; i++) {\n      renderers[i].setIndex(i);\n      rendererCapabilities[i] = renderers[i].getCapabilities();\n    }\n    mediaClock = new DefaultMediaClock(this, clock);\n    pendingMessages = new ArrayList<>();\n    enabledRenderers = new Renderer[0];\n    window = new Timeline.Window();\n    period = new Timeline.Period();\n    trackSelector.init(/* listener= */ this, bandwidthMeter);\n\n    // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states \"Applications can\n    // not normally change to this priority\" is incorrect.\n    internalPlaybackThread =\n        new HandlerThread(\"ExoPlayerImplInternal:Handler\", Process.THREAD_PRIORITY_AUDIO);\n    internalPlaybackThread.start();\n    handler = clock.createHandler(internalPlaybackThread.getLooper(), this);\n    deliverPendingMessageAtStartPositionRequired = true;\n  }\n\n  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {\n    handler\n        .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)\n        .sendToTarget();\n  }\n\n  public void setPlayWhenReady(boolean playWhenReady) {\n    handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();\n  }\n\n  public void setRepeatMode(@Player.RepeatMode int repeatMode) {\n    handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();\n  }\n\n  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {\n    handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget();\n  }\n\n  public void seekTo(Timeline timeline, int windowIndex, long positionUs) {\n    handler\n        .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))\n        .sendToTarget();\n  }\n\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();\n  }\n\n  public void setSeekParameters(SeekParameters seekParameters) {\n    handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();\n  }\n\n  public void stop(boolean reset) {\n    handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();\n  }\n\n  @Override\n  public synchronized void sendMessage(PlayerMessage message) {\n    if (released || !internalPlaybackThread.isAlive()) {\n      Log.w(TAG, \"Ignoring messages sent after release.\");\n      message.markAsProcessed(/* isDelivered= */ false);\n      return;\n    }\n    handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();\n  }\n\n  public synchronized void setForegroundMode(boolean foregroundMode) {\n    if (released || !internalPlaybackThread.isAlive()) {\n      return;\n    }\n    if (foregroundMode) {\n      handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget();\n    } else {\n      AtomicBoolean processedFlag = new AtomicBoolean();\n      handler\n          .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag)\n          .sendToTarget();\n      boolean wasInterrupted = false;\n      while (!processedFlag.get()) {\n        try {\n          wait();\n        } catch (InterruptedException e) {\n          wasInterrupted = true;\n        }\n      }\n      if (wasInterrupted) {\n        // Restore the interrupted status.\n        Thread.currentThread().interrupt();\n      }\n    }\n  }\n\n  public synchronized void release() {\n    if (released || !internalPlaybackThread.isAlive()) {\n      return;\n    }\n    handler.sendEmptyMessage(MSG_RELEASE);\n    boolean wasInterrupted = false;\n    while (!released) {\n      try {\n        wait();\n      } catch (InterruptedException e) {\n        wasInterrupted = true;\n      }\n    }\n    if (wasInterrupted) {\n      // Restore the interrupted status.\n      Thread.currentThread().interrupt();\n    }\n  }\n\n  public Looper getPlaybackLooper() {\n    return internalPlaybackThread.getLooper();\n  }\n\n  // MediaSource.MediaSourceCaller implementation.\n\n  @Override\n  public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {\n    handler\n        .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))\n        .sendToTarget();\n  }\n\n  // MediaPeriod.Callback implementation.\n\n  @Override\n  public void onPrepared(MediaPeriod source) {\n    handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod source) {\n    handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();\n  }\n\n  // TrackSelector.InvalidationListener implementation.\n\n  @Override\n  public void onTrackSelectionsInvalidated() {\n    handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);\n  }\n\n  // DefaultMediaClock.PlaybackParameterListener implementation.\n\n  @Override\n  public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n    sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false);\n  }\n\n  // Handler.Callback implementation.\n\n  @Override\n  public boolean handleMessage(Message msg) {\n    try {\n      switch (msg.what) {\n        case MSG_PREPARE:\n          prepareInternal(\n              (MediaSource) msg.obj,\n              /* resetPosition= */ msg.arg1 != 0,\n              /* resetState= */ msg.arg2 != 0);\n          break;\n        case MSG_SET_PLAY_WHEN_READY:\n          setPlayWhenReadyInternal(msg.arg1 != 0);\n          break;\n        case MSG_SET_REPEAT_MODE:\n          setRepeatModeInternal(msg.arg1);\n          break;\n        case MSG_SET_SHUFFLE_ENABLED:\n          setShuffleModeEnabledInternal(msg.arg1 != 0);\n          break;\n        case MSG_DO_SOME_WORK:\n          doSomeWork();\n          break;\n        case MSG_SEEK_TO:\n          seekToInternal((SeekPosition) msg.obj);\n          break;\n        case MSG_SET_PLAYBACK_PARAMETERS:\n          setPlaybackParametersInternal((PlaybackParameters) msg.obj);\n          break;\n        case MSG_SET_SEEK_PARAMETERS:\n          setSeekParametersInternal((SeekParameters) msg.obj);\n          break;\n        case MSG_SET_FOREGROUND_MODE:\n          setForegroundModeInternal(\n              /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);\n          break;\n        case MSG_STOP:\n          stopInternal(\n              /* forceResetRenderers= */ false,\n              /* resetPositionAndState= */ msg.arg1 != 0,\n              /* acknowledgeStop= */ true);\n          break;\n        case MSG_PERIOD_PREPARED:\n          handlePeriodPrepared((MediaPeriod) msg.obj);\n          break;\n        case MSG_REFRESH_SOURCE_INFO:\n          handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);\n          break;\n        case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:\n          handleContinueLoadingRequested((MediaPeriod) msg.obj);\n          break;\n        case MSG_TRACK_SELECTION_INVALIDATED:\n          reselectTracksInternal();\n          break;\n        case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:\n          handlePlaybackParameters(\n              (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);\n          break;\n        case MSG_SEND_MESSAGE:\n          sendMessageInternal((PlayerMessage) msg.obj);\n          break;\n        case MSG_SEND_MESSAGE_TO_TARGET_THREAD:\n          sendMessageToTargetThread((PlayerMessage) msg.obj);\n          break;\n        case MSG_RELEASE:\n          releaseInternal();\n          // Return immediately to not send playback info updates after release.\n          return true;\n        default:\n          return false;\n      }\n      maybeNotifyPlaybackInfoChanged();\n    } catch (ExoPlaybackException e) {\n      Log.e(TAG, getExoPlaybackExceptionMessage(e), e);\n      stopInternal(\n          /* forceResetRenderers= */ true,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\n      playbackInfo = playbackInfo.copyWithPlaybackError(e);\n      maybeNotifyPlaybackInfoChanged();\n    } catch (IOException e) {\n      Log.e(TAG, \"Source error.\", e);\n      stopInternal(\n          /* forceResetRenderers= */ false,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\n      playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e));\n      maybeNotifyPlaybackInfoChanged();\n    } catch (RuntimeException | OutOfMemoryError e) {\n      Log.e(TAG, \"Internal runtime error.\", e);\n      ExoPlaybackException error =\n          e instanceof OutOfMemoryError\n              ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)\n              : ExoPlaybackException.createForUnexpected((RuntimeException) e);\n      stopInternal(\n          /* forceResetRenderers= */ true,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\n      playbackInfo = playbackInfo.copyWithPlaybackError(error);\n      maybeNotifyPlaybackInfoChanged();\n    }\n    return true;\n  }\n\n  // Private methods.\n\n  private String getExoPlaybackExceptionMessage(ExoPlaybackException e) {\n    if (e.type != ExoPlaybackException.TYPE_RENDERER) {\n      return \"Playback error.\";\n    }\n    return \"Renderer error: index=\"\n        + e.rendererIndex\n        + \", type=\"\n        + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType())\n        + \", format=\"\n        + e.rendererFormat\n        + \", rendererSupport=\"\n        + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport);\n  }\n\n  private void setState(int state) {\n    if (playbackInfo.playbackState != state) {\n      playbackInfo = playbackInfo.copyWithPlaybackState(state);\n    }\n  }\n\n  private void maybeNotifyPlaybackInfoChanged() {\n    if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) {\n      eventHandler\n          .obtainMessage(\n              MSG_PLAYBACK_INFO_CHANGED,\n              playbackInfoUpdate.operationAcks,\n              playbackInfoUpdate.positionDiscontinuity\n                  ? playbackInfoUpdate.discontinuityReason\n                  : C.INDEX_UNSET,\n              playbackInfo)\n          .sendToTarget();\n      playbackInfoUpdate.reset(playbackInfo);\n    }\n  }\n\n  private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {\n    pendingPrepareCount++;\n    resetInternal(\n        /* resetRenderers= */ false,\n        /* releaseMediaSource= */ true,\n        resetPosition,\n        resetState,\n        /* resetError= */ true);\n    loadControl.onPrepared();\n    this.mediaSource = mediaSource;\n    setState(Player.STATE_BUFFERING);\n    mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());\n    handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n  }\n\n  private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {\n    rebuffering = false;\n    this.playWhenReady = playWhenReady;\n    if (!playWhenReady) {\n      stopRenderers();\n      updatePlaybackPositions();\n    } else {\n      if (playbackInfo.playbackState == Player.STATE_READY) {\n        startRenderers();\n        handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n      } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {\n        handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n      }\n    }\n  }\n\n  private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)\n      throws ExoPlaybackException {\n    this.repeatMode = repeatMode;\n    if (!queue.updateRepeatMode(repeatMode)) {\n      seekToCurrentPosition(/* sendDiscontinuity= */ true);\n    }\n    handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\n  }\n\n  private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)\n      throws ExoPlaybackException {\n    this.shuffleModeEnabled = shuffleModeEnabled;\n    if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {\n      seekToCurrentPosition(/* sendDiscontinuity= */ true);\n    }\n    handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\n  }\n\n  private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {\n    // Renderers may have read from a period that's been removed. Seek back to the current\n    // position of the playing period to make sure none of the removed period is played.\n    MediaPeriodId periodId = queue.getPlayingPeriod().info.id;\n    long newPositionUs =\n        seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);\n    if (newPositionUs != playbackInfo.positionUs) {\n      playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);\n      if (sendDiscontinuity) {\n        playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);\n      }\n    }\n  }\n\n  private void startRenderers() throws ExoPlaybackException {\n    rebuffering = false;\n    mediaClock.start();\n    for (Renderer renderer : enabledRenderers) {\n      renderer.start();\n    }\n  }\n\n  private void stopRenderers() throws ExoPlaybackException {\n    mediaClock.stop();\n    for (Renderer renderer : enabledRenderers) {\n      ensureStopped(renderer);\n    }\n  }\n\n  private void updatePlaybackPositions() throws ExoPlaybackException {\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    if (playingPeriodHolder == null) {\n      return;\n    }\n\n    // Update the playback position.\n    long discontinuityPositionUs =\n        playingPeriodHolder.prepared\n            ? playingPeriodHolder.mediaPeriod.readDiscontinuity()\n            : C.TIME_UNSET;\n    if (discontinuityPositionUs != C.TIME_UNSET) {\n      resetRendererPosition(discontinuityPositionUs);\n      // A MediaPeriod may report a discontinuity at the current playback position to ensure the\n      // renderers are flushed. Only report the discontinuity externally if the position changed.\n      if (discontinuityPositionUs != playbackInfo.positionUs) {\n        playbackInfo =\n            copyWithNewPosition(\n                playbackInfo.periodId, discontinuityPositionUs, playbackInfo.contentPositionUs);\n        playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);\n      }\n    } else {\n      rendererPositionUs =\n          mediaClock.syncAndGetPositionUs(\n              /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());\n      long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);\n      maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);\n      playbackInfo.positionUs = periodPositionUs;\n    }\n\n    // Update the buffered position and total buffered duration.\n    MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();\n    playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();\n    playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();\n  }\n\n  private void doSomeWork() throws ExoPlaybackException, IOException {\n    long operationStartTimeMs = clock.uptimeMillis();\n    updatePeriods();\n\n    if (playbackInfo.playbackState == Player.STATE_IDLE\n        || playbackInfo.playbackState == Player.STATE_ENDED) {\n      // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.\n      handler.removeMessages(MSG_DO_SOME_WORK);\n      return;\n    }\n\n    @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    if (playingPeriodHolder == null) {\n      // We're still waiting until the playing period is available.\n      scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);\n      return;\n    }\n\n    TraceUtil.beginSection(\"doSomeWork\");\n\n    updatePlaybackPositions();\n\n    boolean renderersEnded = true;\n    boolean renderersAllowPlayback = true;\n    if (playingPeriodHolder.prepared) {\n      long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;\n      playingPeriodHolder.mediaPeriod.discardBuffer(\n          playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);\n      for (int i = 0; i < renderers.length; i++) {\n        Renderer renderer = renderers[i];\n        if (renderer.getState() == Renderer.STATE_DISABLED) {\n          continue;\n        }\n        // TODO: Each renderer should return the maximum delay before which it wishes to be called\n        // again. The minimum of these values should then be used as the delay before the next\n        // invocation of this method.\n        renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);\n        renderersEnded = renderersEnded && renderer.isEnded();\n        // Determine whether the renderer allows playback to continue. Playback can continue if the\n        // renderer is ready or ended. Also continue playback if the renderer is reading ahead into\n        // the next stream or is waiting for the next stream. This is to avoid getting stuck if\n        // tracks in the current period have uneven durations and are still being read by another\n        // renderer. See: https://github.com/google/ExoPlayer/issues/1874.\n        boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();\n        boolean isWaitingForNextStream =\n            !isReadingAhead\n                && playingPeriodHolder.getNext() != null\n                && renderer.hasReadStreamToEnd();\n        boolean allowsPlayback =\n            isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();\n        renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;\n        if (!allowsPlayback) {\n          renderer.maybeThrowStreamError();\n        }\n      }\n    } else {\n      playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();\n    }\n\n    long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;\n    if (renderersEnded\n        && playingPeriodHolder.prepared\n        && (playingPeriodDurationUs == C.TIME_UNSET\n            || playingPeriodDurationUs <= playbackInfo.positionUs)\n        && playingPeriodHolder.info.isFinal) {\n      setState(Player.STATE_ENDED);\n      stopRenderers();\n    } else if (playbackInfo.playbackState == Player.STATE_BUFFERING\n        && shouldTransitionToReadyState(renderersAllowPlayback)) {\n      setState(Player.STATE_READY);\n      if (playWhenReady) {\n        startRenderers();\n      }\n    } else if (playbackInfo.playbackState == Player.STATE_READY\n        && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) {\n      rebuffering = playWhenReady;\n      setState(Player.STATE_BUFFERING);\n      stopRenderers();\n    }\n\n    if (playbackInfo.playbackState == Player.STATE_BUFFERING) {\n      for (Renderer renderer : enabledRenderers) {\n        renderer.maybeThrowStreamError();\n      }\n    }\n\n    if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)\n        || playbackInfo.playbackState == Player.STATE_BUFFERING) {\n      scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);\n    } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {\n      scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);\n    } else {\n      handler.removeMessages(MSG_DO_SOME_WORK);\n    }\n\n    TraceUtil.endSection();\n  }\n\n  private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {\n    handler.removeMessages(MSG_DO_SOME_WORK);\n    handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);\n  }\n\n  private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {\n    playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);\n\n    MediaPeriodId periodId;\n    long periodPositionUs;\n    long contentPositionUs;\n    boolean seekPositionAdjusted;\n    Pair<Object, Long> resolvedSeekPosition =\n        resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);\n    if (resolvedSeekPosition == null) {\n      // The seek position was valid for the timeline that it was performed into, but the\n      // timeline has changed or is not ready and a suitable seek position could not be resolved.\n      periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);\n      periodPositionUs = C.TIME_UNSET;\n      contentPositionUs = C.TIME_UNSET;\n      seekPositionAdjusted = true;\n    } else {\n      // Update the resolved seek position to take ads into account.\n      Object periodUid = resolvedSeekPosition.first;\n      contentPositionUs = resolvedSeekPosition.second;\n      periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs);\n      if (periodId.isAd()) {\n        periodPositionUs = 0;\n        seekPositionAdjusted = true;\n      } else {\n        periodPositionUs = resolvedSeekPosition.second;\n        seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;\n      }\n    }\n\n    try {\n      if (mediaSource == null || pendingPrepareCount > 0) {\n        // Save seek position for later, as we are still waiting for a prepared source.\n        pendingInitialSeekPosition = seekPosition;\n      } else if (periodPositionUs == C.TIME_UNSET) {\n        // End playback, as we didn't manage to find a valid seek position.\n        setState(Player.STATE_ENDED);\n        resetInternal(\n            /* resetRenderers= */ false,\n            /* releaseMediaSource= */ false,\n            /* resetPosition= */ true,\n            /* resetState= */ false,\n            /* resetError= */ true);\n      } else {\n        // Execute the seek in the current media periods.\n        long newPeriodPositionUs = periodPositionUs;\n        if (periodId.equals(playbackInfo.periodId)) {\n          MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n          if (playingPeriodHolder != null\n              && playingPeriodHolder.prepared\n              && newPeriodPositionUs != 0) {\n            newPeriodPositionUs =\n                playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(\n                    newPeriodPositionUs, seekParameters);\n          }\n          if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)) {\n            // Seek will be performed to the current position. Do nothing.\n            periodPositionUs = playbackInfo.positionUs;\n            return;\n          }\n        }\n        newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs);\n        seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;\n        periodPositionUs = newPeriodPositionUs;\n      }\n    } finally {\n      playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs);\n      if (seekPositionAdjusted) {\n        playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);\n      }\n    }\n  }\n\n  private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs)\n      throws ExoPlaybackException {\n    // Force disable renderers if they are reading from a period other than the one being played.\n    return seekToPeriodPosition(\n        periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod());\n  }\n\n  private long seekToPeriodPosition(\n      MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers)\n      throws ExoPlaybackException {\n    stopRenderers();\n    rebuffering = false;\n    if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) {\n      setState(Player.STATE_BUFFERING);\n    }\n\n    // Clear the timeline, but keep the requested period if it is already prepared.\n    MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();\n    MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;\n    while (newPlayingPeriodHolder != null) {\n      if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) {\n        queue.removeAfter(newPlayingPeriodHolder);\n        break;\n      }\n      newPlayingPeriodHolder = queue.advancePlayingPeriod();\n    }\n\n    // Disable all renderers if the period being played is changing, if the seek results in negative\n    // renderer timestamps, or if forced.\n    if (forceDisableRenderers\n        || oldPlayingPeriodHolder != newPlayingPeriodHolder\n        || (newPlayingPeriodHolder != null\n            && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {\n      for (Renderer renderer : enabledRenderers) {\n        disableRenderer(renderer);\n      }\n      enabledRenderers = new Renderer[0];\n      oldPlayingPeriodHolder = null;\n      if (newPlayingPeriodHolder != null) {\n        newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);\n      }\n    }\n\n    // Update the holders.\n    if (newPlayingPeriodHolder != null) {\n      updatePlayingPeriodRenderers(oldPlayingPeriodHolder);\n      if (newPlayingPeriodHolder.hasEnabledTracks) {\n        periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);\n        newPlayingPeriodHolder.mediaPeriod.discardBuffer(\n            periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);\n      }\n      resetRendererPosition(periodPositionUs);\n      maybeContinueLoading();\n    } else {\n      queue.clear(/* keepFrontPeriodUid= */ true);\n      // New period has not been prepared.\n      playbackInfo =\n          playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult);\n      resetRendererPosition(periodPositionUs);\n    }\n\n    handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\n    handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n    return periodPositionUs;\n  }\n\n  private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {\n    MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod();\n    rendererPositionUs =\n        playingMediaPeriod == null\n            ? periodPositionUs\n            : playingMediaPeriod.toRendererTime(periodPositionUs);\n    mediaClock.resetPosition(rendererPositionUs);\n    for (Renderer renderer : enabledRenderers) {\n      renderer.resetPosition(rendererPositionUs);\n    }\n    notifyTrackSelectionDiscontinuity();\n  }\n\n  private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {\n    mediaClock.setPlaybackParameters(playbackParameters);\n    sendPlaybackParametersChangedInternal(\n        mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);\n  }\n\n  private void setSeekParametersInternal(SeekParameters seekParameters) {\n    this.seekParameters = seekParameters;\n  }\n\n  private void setForegroundModeInternal(\n      boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {\n    if (this.foregroundMode != foregroundMode) {\n      this.foregroundMode = foregroundMode;\n      if (!foregroundMode) {\n        for (Renderer renderer : renderers) {\n          if (renderer.getState() == Renderer.STATE_DISABLED) {\n            renderer.reset();\n          }\n        }\n      }\n    }\n    if (processedFlag != null) {\n      synchronized (this) {\n        processedFlag.set(true);\n        notifyAll();\n      }\n    }\n  }\n\n  private void stopInternal(\n      boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {\n    resetInternal(\n        /* resetRenderers= */ forceResetRenderers || !foregroundMode,\n        /* releaseMediaSource= */ true,\n        /* resetPosition= */ resetPositionAndState,\n        /* resetState= */ resetPositionAndState,\n        /* resetError= */ resetPositionAndState);\n    playbackInfoUpdate.incrementPendingOperationAcks(\n        pendingPrepareCount + (acknowledgeStop ? 1 : 0));\n    pendingPrepareCount = 0;\n    loadControl.onStopped();\n    setState(Player.STATE_IDLE);\n  }\n\n  private void releaseInternal() {\n    resetInternal(\n        /* resetRenderers= */ true,\n        /* releaseMediaSource= */ true,\n        /* resetPosition= */ true,\n        /* resetState= */ true,\n        /* resetError= */ false);\n    loadControl.onReleased();\n    setState(Player.STATE_IDLE);\n    internalPlaybackThread.quit();\n    synchronized (this) {\n      released = true;\n      notifyAll();\n    }\n  }\n\n  private void resetInternal(\n      boolean resetRenderers,\n      boolean releaseMediaSource,\n      boolean resetPosition,\n      boolean resetState,\n      boolean resetError) {\n    handler.removeMessages(MSG_DO_SOME_WORK);\n    rebuffering = false;\n    mediaClock.stop();\n    rendererPositionUs = 0;\n    for (Renderer renderer : enabledRenderers) {\n      try {\n        disableRenderer(renderer);\n      } catch (ExoPlaybackException | RuntimeException e) {\n        // There's nothing we can do.\n        Log.e(TAG, \"Disable failed.\", e);\n      }\n    }\n    if (resetRenderers) {\n      for (Renderer renderer : renderers) {\n        try {\n          renderer.reset();\n        } catch (RuntimeException e) {\n          // There's nothing we can do.\n          Log.e(TAG, \"Reset failed.\", e);\n        }\n      }\n    }\n    enabledRenderers = new Renderer[0];\n\n    if (resetPosition) {\n      pendingInitialSeekPosition = null;\n    } else if (resetState) {\n      // When resetting the state, also reset the period-based PlaybackInfo position and convert\n      // existing position to initial seek instead.\n      resetPosition = true;\n      if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {\n        playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);\n        long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs();\n        pendingInitialSeekPosition =\n            new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs);\n      }\n    }\n\n    queue.clear(/* keepFrontPeriodUid= */ !resetState);\n    shouldContinueLoading = false;\n    if (resetState) {\n      queue.setTimeline(Timeline.EMPTY);\n      for (PendingMessageInfo pendingMessageInfo : pendingMessages) {\n        pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);\n      }\n      pendingMessages.clear();\n      nextPendingMessageIndex = 0;\n    }\n    MediaPeriodId mediaPeriodId =\n        resetPosition\n            ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)\n            : playbackInfo.periodId;\n    // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.\n    long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;\n    long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;\n    playbackInfo =\n        new PlaybackInfo(\n            resetState ? Timeline.EMPTY : playbackInfo.timeline,\n            mediaPeriodId,\n            startPositionUs,\n            contentPositionUs,\n            playbackInfo.playbackState,\n            resetError ? null : playbackInfo.playbackError,\n            /* isLoading= */ false,\n            resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,\n            resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,\n            mediaPeriodId,\n            startPositionUs,\n            /* totalBufferedDurationUs= */ 0,\n            startPositionUs);\n    if (releaseMediaSource) {\n      if (mediaSource != null) {\n        mediaSource.releaseSource(/* caller= */ this);\n        mediaSource = null;\n      }\n    }\n  }\n\n  private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {\n    if (message.getPositionMs() == C.TIME_UNSET) {\n      // If no delivery time is specified, trigger immediate message delivery.\n      sendMessageToTarget(message);\n    } else if (mediaSource == null || pendingPrepareCount > 0) {\n      // Still waiting for initial timeline to resolve position.\n      pendingMessages.add(new PendingMessageInfo(message));\n    } else {\n      PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message);\n      if (resolvePendingMessagePosition(pendingMessageInfo)) {\n        pendingMessages.add(pendingMessageInfo);\n        // Ensure new message is inserted according to playback order.\n        Collections.sort(pendingMessages);\n      } else {\n        message.markAsProcessed(/* isDelivered= */ false);\n      }\n    }\n  }\n\n  private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {\n    if (message.getHandler().getLooper() == handler.getLooper()) {\n      deliverMessage(message);\n      if (playbackInfo.playbackState == Player.STATE_READY\n          || playbackInfo.playbackState == Player.STATE_BUFFERING) {\n        // The message may have caused something to change that now requires us to do work.\n        handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n      }\n    } else {\n      handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget();\n    }\n  }\n\n  private void sendMessageToTargetThread(final PlayerMessage message) {\n    Handler handler = message.getHandler();\n    if (!handler.getLooper().getThread().isAlive()) {\n      Log.w(\"TAG\", \"Trying to send message on a dead thread.\");\n      message.markAsProcessed(/* isDelivered= */ false);\n      return;\n    }\n    handler.post(\n        () -> {\n          try {\n            deliverMessage(message);\n          } catch (ExoPlaybackException e) {\n            Log.e(TAG, \"Unexpected error delivering message on external thread.\", e);\n            throw new RuntimeException(e);\n          }\n        });\n  }\n\n  private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {\n    if (message.isCanceled()) {\n      return;\n    }\n    try {\n      message.getTarget().handleMessage(message.getType(), message.getPayload());\n    } finally {\n      message.markAsProcessed(/* isDelivered= */ true);\n    }\n  }\n\n  private void resolvePendingMessagePositions() {\n    for (int i = pendingMessages.size() - 1; i >= 0; i--) {\n      if (!resolvePendingMessagePosition(pendingMessages.get(i))) {\n        // Unable to resolve a new position for the message. Remove it.\n        pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false);\n        pendingMessages.remove(i);\n      }\n    }\n    // Re-sort messages by playback order.\n    Collections.sort(pendingMessages);\n  }\n\n  private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) {\n    if (pendingMessageInfo.resolvedPeriodUid == null) {\n      // Position is still unresolved. Try to find window in current timeline.\n      Pair<Object, Long> periodPosition =\n          resolveSeekPosition(\n              new SeekPosition(\n                  pendingMessageInfo.message.getTimeline(),\n                  pendingMessageInfo.message.getWindowIndex(),\n                  C.msToUs(pendingMessageInfo.message.getPositionMs())),\n              /* trySubsequentPeriods= */ false);\n      if (periodPosition == null) {\n        return false;\n      }\n      pendingMessageInfo.setResolvedPosition(\n          playbackInfo.timeline.getIndexOfPeriod(periodPosition.first),\n          periodPosition.second,\n          periodPosition.first);\n    } else {\n      // Position has been resolved for a previous timeline. Try to find the updated period index.\n      int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid);\n      if (index == C.INDEX_UNSET) {\n        return false;\n      }\n      pendingMessageInfo.resolvedPeriodIndex = index;\n    }\n    return true;\n  }\n\n  private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)\n      throws ExoPlaybackException {\n    if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {\n      return;\n    }\n    // If this is the first call from the start position, include oldPeriodPositionUs in potential\n    // trigger positions, but make sure we deliver it only once.\n    if (playbackInfo.startPositionUs == oldPeriodPositionUs\n        && deliverPendingMessageAtStartPositionRequired) {\n      oldPeriodPositionUs--;\n    }\n    deliverPendingMessageAtStartPositionRequired = false;\n\n    // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages)\n    int currentPeriodIndex =\n        playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);\n    PendingMessageInfo previousInfo =\n        nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;\n    while (previousInfo != null\n        && (previousInfo.resolvedPeriodIndex > currentPeriodIndex\n            || (previousInfo.resolvedPeriodIndex == currentPeriodIndex\n                && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) {\n      nextPendingMessageIndex--;\n      previousInfo =\n          nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;\n    }\n    PendingMessageInfo nextInfo =\n        nextPendingMessageIndex < pendingMessages.size()\n            ? pendingMessages.get(nextPendingMessageIndex)\n            : null;\n    while (nextInfo != null\n        && nextInfo.resolvedPeriodUid != null\n        && (nextInfo.resolvedPeriodIndex < currentPeriodIndex\n            || (nextInfo.resolvedPeriodIndex == currentPeriodIndex\n                && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) {\n      nextPendingMessageIndex++;\n      nextInfo =\n          nextPendingMessageIndex < pendingMessages.size()\n              ? pendingMessages.get(nextPendingMessageIndex)\n              : null;\n    }\n    // Check if any message falls within the covered time span.\n    while (nextInfo != null\n        && nextInfo.resolvedPeriodUid != null\n        && nextInfo.resolvedPeriodIndex == currentPeriodIndex\n        && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs\n        && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {\n      try {\n        sendMessageToTarget(nextInfo.message);\n      } finally {\n        if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {\n          pendingMessages.remove(nextPendingMessageIndex);\n        } else {\n          nextPendingMessageIndex++;\n        }\n      }\n      nextInfo =\n          nextPendingMessageIndex < pendingMessages.size()\n              ? pendingMessages.get(nextPendingMessageIndex)\n              : null;\n    }\n  }\n\n  private void ensureStopped(Renderer renderer) throws ExoPlaybackException {\n    if (renderer.getState() == Renderer.STATE_STARTED) {\n      renderer.stop();\n    }\n  }\n\n  private void disableRenderer(Renderer renderer) throws ExoPlaybackException {\n    mediaClock.onRendererDisabled(renderer);\n    ensureStopped(renderer);\n    renderer.disable();\n  }\n\n  private void reselectTracksInternal() throws ExoPlaybackException {\n    float playbackSpeed = mediaClock.getPlaybackParameters().speed;\n    // Reselect tracks on each period in turn, until the selection changes.\n    MediaPeriodHolder periodHolder = queue.getPlayingPeriod();\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    boolean selectionsChangedForReadPeriod = true;\n    TrackSelectorResult newTrackSelectorResult;\n    while (true) {\n      if (periodHolder == null || !periodHolder.prepared) {\n        // The reselection did not change any prepared periods.\n        return;\n      }\n      newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);\n      if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {\n        // Selected tracks have changed for this period.\n        break;\n      }\n      if (periodHolder == readingPeriodHolder) {\n        // The track reselection didn't affect any period that has been read.\n        selectionsChangedForReadPeriod = false;\n      }\n      periodHolder = periodHolder.getNext();\n    }\n\n    if (selectionsChangedForReadPeriod) {\n      // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.\n      MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n      boolean recreateStreams = queue.removeAfter(playingPeriodHolder);\n\n      boolean[] streamResetFlags = new boolean[renderers.length];\n      long periodPositionUs =\n          playingPeriodHolder.applyTrackSelection(\n              newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);\n      if (playbackInfo.playbackState != Player.STATE_ENDED\n          && periodPositionUs != playbackInfo.positionUs) {\n        playbackInfo =\n            copyWithNewPosition(\n                playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs);\n        playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);\n        resetRendererPosition(periodPositionUs);\n      }\n\n      int enabledRendererCount = 0;\n      boolean[] rendererWasEnabledFlags = new boolean[renderers.length];\n      for (int i = 0; i < renderers.length; i++) {\n        Renderer renderer = renderers[i];\n        rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;\n        SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];\n        if (sampleStream != null) {\n          enabledRendererCount++;\n        }\n        if (rendererWasEnabledFlags[i]) {\n          if (sampleStream != renderer.getStream()) {\n            // We need to disable the renderer.\n            disableRenderer(renderer);\n          } else if (streamResetFlags[i]) {\n            // The renderer will continue to consume from its current stream, but needs to be reset.\n            renderer.resetPosition(rendererPositionUs);\n          }\n        }\n      }\n      playbackInfo =\n          playbackInfo.copyWithTrackInfo(\n              playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());\n      enableRenderers(rendererWasEnabledFlags, enabledRendererCount);\n    } else {\n      // Release and re-prepare/buffer periods after the one whose selection changed.\n      queue.removeAfter(periodHolder);\n      if (periodHolder.prepared) {\n        long loadingPeriodPositionUs =\n            Math.max(\n                periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));\n        periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);\n      }\n    }\n    handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);\n    if (playbackInfo.playbackState != Player.STATE_ENDED) {\n      maybeContinueLoading();\n      updatePlaybackPositions();\n      handler.sendEmptyMessage(MSG_DO_SOME_WORK);\n    }\n  }\n\n  private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {\n    MediaPeriodHolder periodHolder = queue.getPlayingPeriod();\n    while (periodHolder != null) {\n      TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();\n      for (TrackSelection trackSelection : trackSelections) {\n        if (trackSelection != null) {\n          trackSelection.onPlaybackSpeed(playbackSpeed);\n        }\n      }\n      periodHolder = periodHolder.getNext();\n    }\n  }\n\n  private void notifyTrackSelectionDiscontinuity() {\n    MediaPeriodHolder periodHolder = queue.getPlayingPeriod();\n    while (periodHolder != null) {\n      TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();\n      for (TrackSelection trackSelection : trackSelections) {\n        if (trackSelection != null) {\n          trackSelection.onDiscontinuity();\n        }\n      }\n      periodHolder = periodHolder.getNext();\n    }\n  }\n\n  private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) {\n    if (enabledRenderers.length == 0) {\n      // If there are no enabled renderers, determine whether we're ready based on the timeline.\n      return isTimelineReady();\n    }\n    if (!renderersReadyOrEnded) {\n      return false;\n    }\n    if (!playbackInfo.isLoading) {\n      // Renderers are ready and we're not loading. Transition to ready, since the alternative is\n      // getting stuck waiting for additional media that's not being loaded.\n      return true;\n    }\n    // Renderers are ready and we're loading. Ask the LoadControl whether to transition.\n    MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();\n    boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;\n    return bufferedToEnd\n        || loadControl.shouldStartPlayback(\n            getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);\n  }\n\n  private boolean isTimelineReady() {\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;\n    return playingPeriodHolder.prepared\n        && (playingPeriodDurationUs == C.TIME_UNSET\n            || playbackInfo.positionUs < playingPeriodDurationUs);\n  }\n\n  private void maybeThrowSourceInfoRefreshError() throws IOException {\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    if (loadingPeriodHolder != null) {\n      // Defer throwing until we read all available media periods.\n      for (Renderer renderer : enabledRenderers) {\n        if (!renderer.hasReadStreamToEnd()) {\n          return;\n        }\n      }\n    }\n    mediaSource.maybeThrowSourceInfoRefreshError();\n  }\n\n  private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)\n      throws ExoPlaybackException {\n    if (sourceRefreshInfo.source != mediaSource) {\n      // Stale event.\n      return;\n    }\n    playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);\n    pendingPrepareCount = 0;\n\n    Timeline oldTimeline = playbackInfo.timeline;\n    Timeline timeline = sourceRefreshInfo.timeline;\n    queue.setTimeline(timeline);\n    playbackInfo = playbackInfo.copyWithTimeline(timeline);\n    resolvePendingMessagePositions();\n\n    MediaPeriodId newPeriodId = playbackInfo.periodId;\n    long oldContentPositionUs =\n        playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;\n    long newContentPositionUs = oldContentPositionUs;\n    if (pendingInitialSeekPosition != null) {\n      // Resolve initial seek position.\n      Pair<Object, Long> periodPosition =\n          resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);\n      pendingInitialSeekPosition = null;\n      if (periodPosition == null) {\n        // The seek position was valid for the timeline that it was performed into, but the\n        // timeline has changed and a suitable seek position could not be resolved in the new one.\n        handleSourceInfoRefreshEndedPlayback();\n        return;\n      }\n      newContentPositionUs = periodPosition.second;\n      newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs);\n    } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) {\n      // Resolve unset start position to default position.\n      Pair<Object, Long> defaultPosition =\n          getPeriodPosition(\n              timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);\n      newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second);\n      if (!newPeriodId.isAd()) {\n        // Keep unset start position if we need to play an ad first.\n        newContentPositionUs = defaultPosition.second;\n      }\n    } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {\n      // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose\n      // window we can restart from.\n      Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);\n      if (newPeriodUid == null) {\n        // We failed to resolve a suitable restart position.\n        handleSourceInfoRefreshEndedPlayback();\n        return;\n      }\n      // We resolved a subsequent period. Start at the default position in the corresponding window.\n      Pair<Object, Long> defaultPosition =\n          getPeriodPosition(\n              timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET);\n      newContentPositionUs = defaultPosition.second;\n      newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);\n    } else {\n      // Recheck if the current ad still needs to be played or if we need to start playing an ad.\n      newPeriodId =\n          queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs);\n      if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) {\n        // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and\n        // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential\n        // discontinuity until we reach the former next ad group position.\n        newPeriodId = playbackInfo.periodId;\n      }\n    }\n\n    if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) {\n      // We can keep the current playing period. Update the rest of the queued periods.\n      if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {\n        seekToCurrentPosition(/* sendDiscontinuity= */ false);\n      }\n    } else {\n      // Something changed. Seek to new start position.\n      MediaPeriodHolder periodHolder = queue.getPlayingPeriod();\n      if (periodHolder != null) {\n        // Update the new playing media period info if it already exists.\n        while (periodHolder.getNext() != null) {\n          periodHolder = periodHolder.getNext();\n          if (periodHolder.info.id.equals(newPeriodId)) {\n            periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info);\n          }\n        }\n      }\n      // Actually do the seek.\n      long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;\n      long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs);\n      playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs);\n    }\n    handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\n  }\n\n  private long getMaxRendererReadPositionUs() {\n    MediaPeriodHolder readingHolder = queue.getReadingPeriod();\n    if (readingHolder == null) {\n      return 0;\n    }\n    long maxReadPositionUs = readingHolder.getRendererOffset();\n    if (!readingHolder.prepared) {\n      return maxReadPositionUs;\n    }\n    for (int i = 0; i < renderers.length; i++) {\n      if (renderers[i].getState() == Renderer.STATE_DISABLED\n          || renderers[i].getStream() != readingHolder.sampleStreams[i]) {\n        // Ignore disabled renderers and renderers with sample streams from previous periods.\n        continue;\n      }\n      long readingPositionUs = renderers[i].getReadingPositionUs();\n      if (readingPositionUs == C.TIME_END_OF_SOURCE) {\n        return C.TIME_END_OF_SOURCE;\n      } else {\n        maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);\n      }\n    }\n    return maxReadPositionUs;\n  }\n\n  private void handleSourceInfoRefreshEndedPlayback() {\n    if (playbackInfo.playbackState != Player.STATE_IDLE) {\n      setState(Player.STATE_ENDED);\n    }\n    // Reset, but retain the source so that it can still be used should a seek occur.\n    resetInternal(\n        /* resetRenderers= */ false,\n        /* releaseMediaSource= */ false,\n        /* resetPosition= */ true,\n        /* resetState= */ false,\n        /* resetError= */ true);\n  }\n\n  /**\n   * Given a period index into an old timeline, finds the first subsequent period that also exists\n   * in a new timeline. The uid of this period in the new timeline is returned.\n   *\n   * @param oldPeriodUid The index of the period in the old timeline.\n   * @param oldTimeline The old timeline.\n   * @param newTimeline The new timeline.\n   * @return The uid in the new timeline of the first subsequent period, or null if no such period\n   *     was found.\n   */\n  private @Nullable Object resolveSubsequentPeriod(\n      Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) {\n    int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);\n    int newPeriodIndex = C.INDEX_UNSET;\n    int maxIterations = oldTimeline.getPeriodCount();\n    for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {\n      oldPeriodIndex =\n          oldTimeline.getNextPeriodIndex(\n              oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled);\n      if (oldPeriodIndex == C.INDEX_UNSET) {\n        // We've reached the end of the old timeline.\n        break;\n      }\n      newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));\n    }\n    return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);\n  }\n\n  /**\n   * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the\n   * internal timeline.\n   *\n   * @param seekPosition The position to resolve.\n   * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching\n   *     period if the original period is no longer available.\n   * @return The resolved position, or null if resolution was not successful.\n   * @throws IllegalSeekPositionException If the window index of the seek position is outside the\n   *     bounds of the timeline.\n   */\n  @Nullable\n  private Pair<Object, Long> resolveSeekPosition(\n      SeekPosition seekPosition, boolean trySubsequentPeriods) {\n    Timeline timeline = playbackInfo.timeline;\n    Timeline seekTimeline = seekPosition.timeline;\n    if (timeline.isEmpty()) {\n      // We don't have a valid timeline yet, so we can't resolve the position.\n      return null;\n    }\n    if (seekTimeline.isEmpty()) {\n      // The application performed a blind seek with an empty timeline (most likely based on\n      // knowledge of what the future timeline will be). Use the internal timeline.\n      seekTimeline = timeline;\n    }\n    // Map the SeekPosition to a position in the corresponding timeline.\n    Pair<Object, Long> periodPosition;\n    try {\n      periodPosition =\n          seekTimeline.getPeriodPosition(\n              window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);\n    } catch (IndexOutOfBoundsException e) {\n      // The window index of the seek position was outside the bounds of the timeline.\n      return null;\n    }\n    if (timeline == seekTimeline) {\n      // Our internal timeline is the seek timeline, so the mapped position is correct.\n      return periodPosition;\n    }\n    // Attempt to find the mapped period in the internal timeline.\n    int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);\n    if (periodIndex != C.INDEX_UNSET) {\n      // We successfully located the period in the internal timeline.\n      return periodPosition;\n    }\n    if (trySubsequentPeriods) {\n      // Try and find a subsequent period from the seek timeline in the internal timeline.\n      @Nullable\n      Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);\n      if (periodUid != null) {\n        // We found one. Use the default position of the corresponding window.\n        return getPeriodPosition(\n            timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET);\n      }\n    }\n    // We didn't find one. Give up.\n    return null;\n  }\n\n  /**\n   * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the\n   * current timeline.\n   */\n  private Pair<Object, Long> getPeriodPosition(\n      Timeline timeline, int windowIndex, long windowPositionUs) {\n    return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);\n  }\n\n  private void updatePeriods() throws ExoPlaybackException, IOException {\n    if (mediaSource == null) {\n      // The player has no media source yet.\n      return;\n    }\n    if (pendingPrepareCount > 0) {\n      // We're waiting to get information about periods.\n      mediaSource.maybeThrowSourceInfoRefreshError();\n      return;\n    }\n    maybeUpdateLoadingPeriod();\n    maybeUpdateReadingPeriod();\n    maybeUpdatePlayingPeriod();\n  }\n\n  private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException {\n    queue.reevaluateBuffer(rendererPositionUs);\n    if (queue.shouldLoadNextMediaPeriod()) {\n      MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);\n      if (info == null) {\n        maybeThrowSourceInfoRefreshError();\n      } else {\n        MediaPeriodHolder mediaPeriodHolder =\n            queue.enqueueNextMediaPeriodHolder(\n                rendererCapabilities,\n                trackSelector,\n                loadControl.getAllocator(),\n                mediaSource,\n                info,\n                emptyTrackSelectorResult);\n        mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);\n        if (queue.getPlayingPeriod() == mediaPeriodHolder) {\n          resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime());\n        }\n        handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\n      }\n    }\n    if (shouldContinueLoading) {\n      shouldContinueLoading = isLoadingPossible();\n      updateIsLoading();\n    } else {\n      maybeContinueLoading();\n    }\n  }\n\n  private void maybeUpdateReadingPeriod() throws ExoPlaybackException {\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    if (readingPeriodHolder == null) {\n      return;\n    }\n\n    if (readingPeriodHolder.getNext() == null) {\n      // We don't have a successor to advance the reading period to.\n      if (readingPeriodHolder.info.isFinal) {\n        for (int i = 0; i < renderers.length; i++) {\n          Renderer renderer = renderers[i];\n          SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];\n          // Defer setting the stream as final until the renderer has actually consumed the whole\n          // stream in case of playlist changes that cause the stream to be no longer final.\n          if (sampleStream != null\n              && renderer.getStream() == sampleStream\n              && renderer.hasReadStreamToEnd()) {\n            renderer.setCurrentStreamFinal();\n          }\n        }\n      }\n      return;\n    }\n\n    if (!hasReadingPeriodFinishedReading()) {\n      return;\n    }\n\n    if (!readingPeriodHolder.getNext().prepared) {\n      // The successor is not prepared yet.\n      return;\n    }\n\n    TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();\n    readingPeriodHolder = queue.advanceReadingPeriod();\n    TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();\n\n    if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {\n      // The new period starts with a discontinuity, so the renderers will play out all data, then\n      // be disabled and re-enabled when they start playing the next period.\n      setAllRendererStreamsFinal();\n      return;\n    }\n    for (int i = 0; i < renderers.length; i++) {\n      Renderer renderer = renderers[i];\n      boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i);\n      if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) {\n        // The renderer is enabled and its stream is not final, so we still have a chance to replace\n        // the sample streams.\n        TrackSelection newSelection = newTrackSelectorResult.selections.get(i);\n        boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i);\n        boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;\n        RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];\n        RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];\n        if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) {\n          // Replace the renderer's SampleStream so the transition to playing the next period can\n          // be seamless.\n          // This should be avoided for no-sample renderer, because skipping ahead for such\n          // renderer doesn't have any benefit (the renderer does not consume the sample stream),\n          // and it will change the provided rendererOffsetUs while the renderer is still\n          // rendering from the playing media period.\n          Format[] formats = getFormats(newSelection);\n          renderer.replaceStream(\n              formats,\n              readingPeriodHolder.sampleStreams[i],\n              readingPeriodHolder.getRendererOffset());\n        } else {\n          // The renderer will be disabled when transitioning to playing the next period, because\n          // there's no new selection, or because a configuration change is required, or because\n          // it's a no-sample renderer for which rendererOffsetUs should be updated only when\n          // starting to play the next period. Mark the SampleStream as final to play out any\n          // remaining data.\n          renderer.setCurrentStreamFinal();\n        }\n      }\n    }\n  }\n\n  private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {\n    boolean advancedPlayingPeriod = false;\n    while (shouldAdvancePlayingPeriod()) {\n      if (advancedPlayingPeriod) {\n        // If we advance more than one period at a time, notify listeners after each update.\n        maybeNotifyPlaybackInfoChanged();\n      }\n      MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();\n      if (oldPlayingPeriodHolder == queue.getReadingPeriod()) {\n        // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams\n        // anymore and need to re-enable the renderers. Set all current streams final to do that.\n        setAllRendererStreamsFinal();\n      }\n      MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();\n      updatePlayingPeriodRenderers(oldPlayingPeriodHolder);\n      playbackInfo =\n          copyWithNewPosition(\n              newPlayingPeriodHolder.info.id,\n              newPlayingPeriodHolder.info.startPositionUs,\n              newPlayingPeriodHolder.info.contentPositionUs);\n      int discontinuityReason =\n          oldPlayingPeriodHolder.info.isLastInTimelinePeriod\n              ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION\n              : Player.DISCONTINUITY_REASON_AD_INSERTION;\n      playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);\n      updatePlaybackPositions();\n      advancedPlayingPeriod = true;\n    }\n  }\n\n  private boolean shouldAdvancePlayingPeriod() {\n    if (!playWhenReady) {\n      return false;\n    }\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    if (playingPeriodHolder == null) {\n      return false;\n    }\n    MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();\n    if (nextPlayingPeriodHolder == null) {\n      return false;\n    }\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) {\n      return false;\n    }\n    return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime();\n  }\n\n  private boolean hasReadingPeriodFinishedReading() {\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    if (!readingPeriodHolder.prepared) {\n      return false;\n    }\n    for (int i = 0; i < renderers.length; i++) {\n      Renderer renderer = renderers[i];\n      SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];\n      if (renderer.getStream() != sampleStream\n          || (sampleStream != null && !renderer.hasReadStreamToEnd())) {\n        // The current reading period is still being read by at least one renderer.\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private void setAllRendererStreamsFinal() {\n    for (Renderer renderer : renderers) {\n      if (renderer.getStream() != null) {\n        renderer.setCurrentStreamFinal();\n      }\n    }\n  }\n\n  private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {\n    if (!queue.isLoading(mediaPeriod)) {\n      // Stale event.\n      return;\n    }\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    loadingPeriodHolder.handlePrepared(\n        mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);\n    updateLoadControlTrackSelection(\n        loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());\n    if (loadingPeriodHolder == queue.getPlayingPeriod()) {\n      // This is the first prepared period, so update the position and the renderers.\n      resetRendererPosition(loadingPeriodHolder.info.startPositionUs);\n      updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null);\n    }\n    maybeContinueLoading();\n  }\n\n  private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {\n    if (!queue.isLoading(mediaPeriod)) {\n      // Stale event.\n      return;\n    }\n    queue.reevaluateBuffer(rendererPositionUs);\n    maybeContinueLoading();\n  }\n\n  private void handlePlaybackParameters(\n      PlaybackParameters playbackParameters, boolean acknowledgeCommand)\n      throws ExoPlaybackException {\n    eventHandler\n        .obtainMessage(\n            MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters)\n        .sendToTarget();\n    updateTrackSelectionPlaybackSpeed(playbackParameters.speed);\n    for (Renderer renderer : renderers) {\n      if (renderer != null) {\n        renderer.setOperatingRate(playbackParameters.speed);\n      }\n    }\n  }\n\n  private void maybeContinueLoading() {\n    shouldContinueLoading = shouldContinueLoading();\n    if (shouldContinueLoading) {\n      queue.getLoadingPeriod().continueLoading(rendererPositionUs);\n    }\n    updateIsLoading();\n  }\n\n  private boolean shouldContinueLoading() {\n    if (!isLoadingPossible()) {\n      return false;\n    }\n    long bufferedDurationUs =\n        getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs());\n    float playbackSpeed = mediaClock.getPlaybackParameters().speed;\n    return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);\n  }\n\n  private boolean isLoadingPossible() {\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    if (loadingPeriodHolder == null) {\n      return false;\n    }\n    long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();\n    if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {\n      return false;\n    }\n    return true;\n  }\n\n  private void updateIsLoading() {\n    MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();\n    boolean isLoading =\n        shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading());\n    if (isLoading != playbackInfo.isLoading) {\n      playbackInfo = playbackInfo.copyWithIsLoading(isLoading);\n    }\n  }\n\n  private PlaybackInfo copyWithNewPosition(\n      MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {\n    deliverPendingMessageAtStartPositionRequired = true;\n    return playbackInfo.copyWithNewPosition(\n        mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs());\n  }\n\n  @SuppressWarnings(\"ParameterNotNullable\")\n  private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)\n      throws ExoPlaybackException {\n    MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();\n    if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) {\n      return;\n    }\n    int enabledRendererCount = 0;\n    boolean[] rendererWasEnabledFlags = new boolean[renderers.length];\n    for (int i = 0; i < renderers.length; i++) {\n      Renderer renderer = renderers[i];\n      rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;\n      if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {\n        enabledRendererCount++;\n      }\n      if (rendererWasEnabledFlags[i]\n          && (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)\n              || (renderer.isCurrentStreamFinal()\n                  && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {\n        // The renderer should be disabled before playing the next period, either because it's not\n        // needed to play the next period, or because we need to re-enable it as its current stream\n        // is final and it's not reading ahead.\n        disableRenderer(renderer);\n      }\n    }\n    playbackInfo =\n        playbackInfo.copyWithTrackInfo(\n            newPlayingPeriodHolder.getTrackGroups(),\n            newPlayingPeriodHolder.getTrackSelectorResult());\n    enableRenderers(rendererWasEnabledFlags, enabledRendererCount);\n  }\n\n  private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount)\n      throws ExoPlaybackException {\n    enabledRenderers = new Renderer[totalEnabledRendererCount];\n    int enabledRendererCount = 0;\n    TrackSelectorResult trackSelectorResult = queue.getPlayingPeriod().getTrackSelectorResult();\n    // Reset all disabled renderers before enabling any new ones. This makes sure resources released\n    // by the disabled renderers will be available to renderers that are being enabled.\n    for (int i = 0; i < renderers.length; i++) {\n      if (!trackSelectorResult.isRendererEnabled(i)) {\n        renderers[i].reset();\n      }\n    }\n    // Enable the renderers.\n    for (int i = 0; i < renderers.length; i++) {\n      if (trackSelectorResult.isRendererEnabled(i)) {\n        enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++);\n      }\n    }\n  }\n\n  private void enableRenderer(\n      int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex)\n      throws ExoPlaybackException {\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    Renderer renderer = renderers[rendererIndex];\n    enabledRenderers[enabledRendererIndex] = renderer;\n    if (renderer.getState() == Renderer.STATE_DISABLED) {\n      TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult();\n      RendererConfiguration rendererConfiguration =\n          trackSelectorResult.rendererConfigurations[rendererIndex];\n      TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex);\n      Format[] formats = getFormats(newSelection);\n      // The renderer needs enabling with its new track selection.\n      boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY;\n      // Consider as joining only if the renderer was previously disabled.\n      boolean joining = !wasRendererEnabled && playing;\n      // Enable the renderer.\n      renderer.enable(\n          rendererConfiguration,\n          formats,\n          playingPeriodHolder.sampleStreams[rendererIndex],\n          rendererPositionUs,\n          joining,\n          playingPeriodHolder.getRendererOffset());\n      mediaClock.onRendererEnabled(renderer);\n      // Start the renderer if playing.\n      if (playing) {\n        renderer.start();\n      }\n    }\n  }\n\n  private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) {\n    MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();\n    MediaPeriodId loadingMediaPeriodId =\n        loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id;\n    boolean loadingMediaPeriodChanged =\n        !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId);\n    if (loadingMediaPeriodChanged) {\n      playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);\n    }\n    playbackInfo.bufferedPositionUs =\n        loadingMediaPeriodHolder == null\n            ? playbackInfo.positionUs\n            : loadingMediaPeriodHolder.getBufferedPositionUs();\n    playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();\n    if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged)\n        && loadingMediaPeriodHolder != null\n        && loadingMediaPeriodHolder.prepared) {\n      updateLoadControlTrackSelection(\n          loadingMediaPeriodHolder.getTrackGroups(),\n          loadingMediaPeriodHolder.getTrackSelectorResult());\n    }\n  }\n\n  private long getTotalBufferedDurationUs() {\n    return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs);\n  }\n\n  private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    if (loadingPeriodHolder == null) {\n      return 0;\n    }\n    long totalBufferedDurationUs =\n        bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);\n    return Math.max(0, totalBufferedDurationUs);\n  }\n\n  private void updateLoadControlTrackSelection(\n      TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {\n    loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);\n  }\n\n  private void sendPlaybackParametersChangedInternal(\n      PlaybackParameters playbackParameters, boolean acknowledgeCommand) {\n    handler\n        .obtainMessage(\n            MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL,\n            acknowledgeCommand ? 1 : 0,\n            0,\n            playbackParameters)\n        .sendToTarget();\n  }\n\n  private static Format[] getFormats(TrackSelection newSelection) {\n    // Build an array of formats contained by the selection.\n    int length = newSelection != null ? newSelection.length() : 0;\n    Format[] formats = new Format[length];\n    for (int i = 0; i < length; i++) {\n      formats[i] = newSelection.getFormat(i);\n    }\n    return formats;\n  }\n\n  private static final class SeekPosition {\n\n    public final Timeline timeline;\n    public final int windowIndex;\n    public final long windowPositionUs;\n\n    public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {\n      this.timeline = timeline;\n      this.windowIndex = windowIndex;\n      this.windowPositionUs = windowPositionUs;\n    }\n  }\n\n  private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> {\n\n    public final PlayerMessage message;\n\n    public int resolvedPeriodIndex;\n    public long resolvedPeriodTimeUs;\n    @Nullable public Object resolvedPeriodUid;\n\n    public PendingMessageInfo(PlayerMessage message) {\n      this.message = message;\n    }\n\n    public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) {\n      resolvedPeriodIndex = periodIndex;\n      resolvedPeriodTimeUs = periodTimeUs;\n      resolvedPeriodUid = periodUid;\n    }\n\n    @Override\n    public int compareTo(PendingMessageInfo other) {\n      if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) {\n        // PendingMessageInfos with a resolved period position are always smaller.\n        return resolvedPeriodUid != null ? -1 : 1;\n      }\n      if (resolvedPeriodUid == null) {\n        // Don't sort message with unresolved positions.\n        return 0;\n      }\n      // Sort resolved media times by period index and then by period position.\n      int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex;\n      if (comparePeriodIndex != 0) {\n        return comparePeriodIndex;\n      }\n      return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs);\n    }\n  }\n\n  private static final class MediaSourceRefreshInfo {\n\n    public final MediaSource source;\n    public final Timeline timeline;\n\n    public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {\n      this.source = source;\n      this.timeline = timeline;\n    }\n  }\n\n  private static final class PlaybackInfoUpdate {\n\n    private PlaybackInfo lastPlaybackInfo;\n    private int operationAcks;\n    private boolean positionDiscontinuity;\n    private @DiscontinuityReason int discontinuityReason;\n\n    public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {\n      return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;\n    }\n\n    public void reset(PlaybackInfo playbackInfo) {\n      lastPlaybackInfo = playbackInfo;\n      operationAcks = 0;\n      positionDiscontinuity = false;\n    }\n\n    public void incrementPendingOperationAcks(int operationAcks) {\n      this.operationAcks += operationAcks;\n    }\n\n    public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) {\n      if (positionDiscontinuity\n          && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) {\n        // We always prefer non-internal discontinuity reasons. We also assume that we won't report\n        // more than one non-internal discontinuity per message iteration.\n        Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL);\n        return;\n      }\n      positionDiscontinuity = true;\n      this.discontinuityReason = discontinuityReason;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport java.util.HashSet;\n\n/**\n * Information about the ExoPlayer library.\n */\npublic final class ExoPlayerLibraryInfo {\n\n  /**\n   * A tag to use when logging library information.\n   */\n  public static final String TAG = \"ExoPlayer\";\n\n  /** The version of the library expressed as a string, for example \"1.2.3\". */\n  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.\n  public static final String VERSION = \"2.11.1\";\n\n  /** The version of the library expressed as {@code \"ExoPlayerLib/\" + VERSION}. */\n  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.\n  public static final String VERSION_SLASHY = \"ExoPlayerLib/2.11.1\";\n\n  /**\n   * The version of the library expressed as an integer, for example 1002003.\n   *\n   * <p>Three digits are used for each component of {@link #VERSION}. For example \"1.2.3\" has the\n   * corresponding integer version 1002003 (001-002-003), and \"123.45.6\" has the corresponding\n   * integer version 123045006 (123-045-006).\n   */\n  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.\n  public static final int VERSION_INT = 2011001;\n\n  /**\n   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}\n   * checks enabled.\n   */\n  public static final boolean ASSERTIONS_ENABLED = true;\n\n  /** Whether an exception should be thrown in case of an OpenGl error. */\n  public static final boolean GL_ASSERTIONS_ENABLED = false;\n\n  /**\n   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}\n   * trace enabled.\n   */\n  public static final boolean TRACE_ENABLED = true;\n\n  private static final HashSet<String> registeredModules = new HashSet<>();\n  private static String registeredModulesString = \"goog.exo.core\";\n\n  private ExoPlayerLibraryInfo() {} // Prevents instantiation.\n\n  /**\n   * Returns a string consisting of registered module names separated by \", \".\n   */\n  public static synchronized String registeredModules() {\n    return registeredModulesString;\n  }\n\n  /**\n   * Registers a module to be returned in the {@link #registeredModules()} string.\n   *\n   * @param name The name of the module being registered.\n   */\n  public static synchronized void registerModule(String name) {\n    if (registeredModules.add(name)) {\n      registeredModulesString = registeredModulesString + \", \" + name;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/Format.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Representation of a media format.\n */\npublic final class Format implements Parcelable {\n\n  /**\n   * A value for various fields to indicate that the field's value is unknown or not applicable.\n   */\n  public static final int NO_VALUE = -1;\n\n  /**\n   * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to\n   * the timestamps of their parent samples.\n   */\n  public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;\n\n  /** An identifier for the format, or null if unknown or not applicable. */\n  @Nullable public final String id;\n  /** The human readable label, or null if unknown or not applicable. */\n  @Nullable public final String label;\n  /** Track selection flags. */\n  @C.SelectionFlags public final int selectionFlags;\n  /** Track role flags. */\n  @C.RoleFlags public final int roleFlags;\n  /**\n   * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final int bitrate;\n  /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */\n  @Nullable public final String codecs;\n  /** Metadata, or null if unknown or not applicable. */\n  @Nullable public final Metadata metadata;\n\n  // Container specific.\n\n  /** The mime type of the container, or null if unknown or not applicable. */\n  @Nullable public final String containerMimeType;\n\n  // Elementary stream specific.\n\n  /**\n   * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not\n   * applicable.\n   */\n  @Nullable public final String sampleMimeType;\n  /**\n   * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or\n   * not applicable.\n   */\n  public final int maxInputSize;\n  /**\n   * Initialization data that must be provided to the decoder. Will not be null, but may be empty\n   * if initialization data is not required.\n   */\n  public final List<byte[]> initializationData;\n  /** DRM initialization data if the stream is protected, or null otherwise. */\n  @Nullable public final DrmInitData drmInitData;\n\n  /**\n   * For samples that contain subsamples, this is an offset that should be added to subsample\n   * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are\n   * relative to the timestamps of their parent samples.\n   */\n  public final long subsampleOffsetUs;\n\n  // Video specific.\n\n  /**\n   * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final int width;\n  /**\n   * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final int height;\n  /**\n   * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final float frameRate;\n  /**\n   * The clockwise rotation that should be applied to the video for it to be rendered in the correct\n   * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported.\n   */\n  public final int rotationDegrees;\n  /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */\n  public final float pixelWidthHeightRatio;\n  /**\n   * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo\n   * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link\n   * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.\n   */\n  @C.StereoMode\n  public final int stereoMode;\n  /** The projection data for 360/VR video, or null if not applicable. */\n  @Nullable public final byte[] projectionData;\n  /** The color metadata associated with the video, helps with accurate color reproduction. */\n  @Nullable public final ColorInfo colorInfo;\n\n  // Audio specific.\n\n  /**\n   * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final int channelCount;\n  /**\n   * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.\n   */\n  public final int sampleRate;\n  /**\n   * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}\n   * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link\n   * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link\n   * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other\n   * media types.\n   */\n  public final @C.PcmEncoding int pcmEncoding;\n  /**\n   * The number of frames to trim from the start of the decoded audio stream, or 0 if not\n   * applicable.\n   */\n  public final int encoderDelay;\n  /**\n   * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable.\n   */\n  public final int encoderPadding;\n\n  // Audio and text specific.\n\n  /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */\n  @Nullable public final String language;\n  /**\n   * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.\n   */\n  public final int accessibilityChannel;\n\n  // Provided by source.\n\n  /**\n   * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can\n   * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire\n   * a session for {@link #drmInitData}, or if not applicable.\n   */\n  @Nullable public final Class<? extends ExoMediaCrypto> exoMediaCryptoType;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  // Video.\n\n  /**\n   * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String,\n   *     Metadata, int, int, int, float, List, int, int)} instead.\n   */\n  @Deprecated\n  public static Format createVideoContainerFormat(\n      @Nullable String id,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int width,\n      int height,\n      float frameRate,\n      @Nullable List<byte[]> initializationData,\n      @C.SelectionFlags int selectionFlags) {\n    return createVideoContainerFormat(\n        id,\n        /* label= */ null,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        /* metadata= */ null,\n        bitrate,\n        width,\n        height,\n        frameRate,\n        initializationData,\n        selectionFlags,\n        /* roleFlags= */ 0);\n  }\n\n  public static Format createVideoContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      @Nullable Metadata metadata,\n      int bitrate,\n      int width,\n      int height,\n      float frameRate,\n      @Nullable List<byte[]> initializationData,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        initializationData,\n        /* drmInitData= */ null,\n        OFFSET_SAMPLE_RELATIVE,\n        width,\n        height,\n        frameRate,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        /* language= */ null,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  public static Format createVideoSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int width,\n      int height,\n      float frameRate,\n      @Nullable List<byte[]> initializationData,\n      @Nullable DrmInitData drmInitData) {\n    return createVideoSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        maxInputSize,\n        width,\n        height,\n        frameRate,\n        initializationData,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        drmInitData);\n  }\n\n  public static Format createVideoSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int width,\n      int height,\n      float frameRate,\n      @Nullable List<byte[]> initializationData,\n      int rotationDegrees,\n      float pixelWidthHeightRatio,\n      @Nullable DrmInitData drmInitData) {\n    return createVideoSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        maxInputSize,\n        width,\n        height,\n        frameRate,\n        initializationData,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        drmInitData);\n  }\n\n  public static Format createVideoSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int width,\n      int height,\n      float frameRate,\n      @Nullable List<byte[]> initializationData,\n      int rotationDegrees,\n      float pixelWidthHeightRatio,\n      @Nullable byte[] projectionData,\n      @C.StereoMode int stereoMode,\n      @Nullable ColorInfo colorInfo,\n      @Nullable DrmInitData drmInitData) {\n    return new Format(\n        id,\n        /* label= */ null,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        bitrate,\n        codecs,\n        /* metadata= */ null,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        /* language= */ null,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  // Audio.\n\n  /**\n   * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String,\n   *     Metadata, int, int, int, List, int, int, String)} instead.\n   */\n  @Deprecated\n  public static Format createAudioContainerFormat(\n      @Nullable String id,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int channelCount,\n      int sampleRate,\n      @Nullable List<byte[]> initializationData,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n    return createAudioContainerFormat(\n        id,\n        /* label= */ null,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        /* metadata= */ null,\n        bitrate,\n        channelCount,\n        sampleRate,\n        initializationData,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        language);\n  }\n\n  public static Format createAudioContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      @Nullable Metadata metadata,\n      int bitrate,\n      int channelCount,\n      int sampleRate,\n      @Nullable List<byte[]> initializationData,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags,\n      @Nullable String language) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        initializationData,\n        /* drmInitData= */ null,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        channelCount,\n        sampleRate,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        language,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  public static Format createAudioSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int channelCount,\n      int sampleRate,\n      @Nullable List<byte[]> initializationData,\n      @Nullable DrmInitData drmInitData,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n    return createAudioSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        maxInputSize,\n        channelCount,\n        sampleRate,\n        /* pcmEncoding= */ NO_VALUE,\n        initializationData,\n        drmInitData,\n        selectionFlags,\n        language);\n  }\n\n  public static Format createAudioSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int channelCount,\n      int sampleRate,\n      @C.PcmEncoding int pcmEncoding,\n      @Nullable List<byte[]> initializationData,\n      @Nullable DrmInitData drmInitData,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n    return createAudioSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        maxInputSize,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        initializationData,\n        drmInitData,\n        selectionFlags,\n        language,\n        /* metadata= */ null);\n  }\n\n  public static Format createAudioSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      int maxInputSize,\n      int channelCount,\n      int sampleRate,\n      @C.PcmEncoding int pcmEncoding,\n      int encoderDelay,\n      int encoderPadding,\n      @Nullable List<byte[]> initializationData,\n      @Nullable DrmInitData drmInitData,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language,\n      @Nullable Metadata metadata) {\n    return new Format(\n        id,\n        /* label= */ null,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        bitrate,\n        codecs,\n        metadata,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  // Text.\n\n  public static Format createTextContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags,\n      @Nullable String language) {\n    return createTextContainerFormat(\n        id,\n        label,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        selectionFlags,\n        roleFlags,\n        language,\n        /* accessibilityChannel= */ NO_VALUE);\n  }\n\n  public static Format createTextContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags,\n      @Nullable String language,\n      int accessibilityChannel) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        /* metadata= */ null,\n        containerMimeType,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        language,\n        accessibilityChannel,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n    return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null);\n  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language,\n      @Nullable DrmInitData drmInitData) {\n    return createTextSampleFormat(\n        id,\n        sampleMimeType,\n        /* codecs= */ null,\n        /* bitrate= */ NO_VALUE,\n        selectionFlags,\n        language,\n        NO_VALUE,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        Collections.emptyList());\n  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language,\n      int accessibilityChannel,\n      @Nullable DrmInitData drmInitData) {\n    return createTextSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        selectionFlags,\n        language,\n        accessibilityChannel,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        Collections.emptyList());\n  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language,\n      @Nullable DrmInitData drmInitData,\n      long subsampleOffsetUs) {\n    return createTextSampleFormat(\n        id,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        selectionFlags,\n        language,\n        /* accessibilityChannel= */ NO_VALUE,\n        drmInitData,\n        subsampleOffsetUs,\n        Collections.emptyList());\n  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language,\n      int accessibilityChannel,\n      @Nullable DrmInitData drmInitData,\n      long subsampleOffsetUs,\n      @Nullable List<byte[]> initializationData) {\n    return new Format(\n        id,\n        /* label= */ null,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        bitrate,\n        codecs,\n        /* metadata= */ null,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        language,\n        accessibilityChannel,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  // Image.\n\n  public static Format createImageSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable List<byte[]> initializationData,\n      @Nullable String language,\n      @Nullable DrmInitData drmInitData) {\n    return new Format(\n        id,\n        /* label= */ null,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        bitrate,\n        codecs,\n        /* metadata=*/ null,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        initializationData,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        language,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  // Generic.\n\n  /**\n   * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int,\n   *     int, String)} instead.\n   */\n  @Deprecated\n  public static Format createContainerFormat(\n      @Nullable String id,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n    return createContainerFormat(\n        id,\n        /* label= */ null,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        language);\n  }\n\n  public static Format createContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags,\n      @Nullable String language) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        /* metadata= */ null,\n        containerMimeType,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        language,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  public static Format createSampleFormat(\n      @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) {\n    return new Format(\n        id,\n        /* label= */ null,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        /* bitrate= */ NO_VALUE,\n        /* codecs= */ null,\n        /* metadata= */ null,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null,\n        subsampleOffsetUs,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        /* language= */ null,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  public static Format createSampleFormat(\n      @Nullable String id,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      int bitrate,\n      @Nullable DrmInitData drmInitData) {\n    return new Format(\n        id,\n        /* label= */ null,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        bitrate,\n        codecs,\n        /* metadata= */ null,\n        /* containerMimeType= */ null,\n        sampleMimeType,\n        /* maxInputSize= */ NO_VALUE,\n        /* initializationData= */ null,\n        drmInitData,\n        OFFSET_SAMPLE_RELATIVE,\n        /* width= */ NO_VALUE,\n        /* height= */ NO_VALUE,\n        /* frameRate= */ NO_VALUE,\n        /* rotationDegrees= */ NO_VALUE,\n        /* pixelWidthHeightRatio= */ NO_VALUE,\n        /* projectionData= */ null,\n        /* stereoMode= */ NO_VALUE,\n        /* colorInfo= */ null,\n        /* channelCount= */ NO_VALUE,\n        /* sampleRate= */ NO_VALUE,\n        /* pcmEncoding= */ NO_VALUE,\n        /* encoderDelay= */ NO_VALUE,\n        /* encoderPadding= */ NO_VALUE,\n        /* language= */ null,\n        /* accessibilityChannel= */ NO_VALUE,\n        /* exoMediaCryptoType= */ null);\n  }\n\n  /* package */ Format(\n      @Nullable String id,\n      @Nullable String label,\n      @C.SelectionFlags int selectionFlags,\n      @C.RoleFlags int roleFlags,\n      int bitrate,\n      @Nullable String codecs,\n      @Nullable Metadata metadata,\n      // Container specific.\n      @Nullable String containerMimeType,\n      // Elementary stream specific.\n      @Nullable String sampleMimeType,\n      int maxInputSize,\n      @Nullable List<byte[]> initializationData,\n      @Nullable DrmInitData drmInitData,\n      long subsampleOffsetUs,\n      // Video specific.\n      int width,\n      int height,\n      float frameRate,\n      int rotationDegrees,\n      float pixelWidthHeightRatio,\n      @Nullable byte[] projectionData,\n      @C.StereoMode int stereoMode,\n      @Nullable ColorInfo colorInfo,\n      // Audio specific.\n      int channelCount,\n      int sampleRate,\n      @C.PcmEncoding int pcmEncoding,\n      int encoderDelay,\n      int encoderPadding,\n      // Audio and text specific.\n      @Nullable String language,\n      int accessibilityChannel,\n      // Provided by source.\n      @Nullable Class<? extends ExoMediaCrypto> exoMediaCryptoType) {\n    this.id = id;\n    this.label = label;\n    this.selectionFlags = selectionFlags;\n    this.roleFlags = roleFlags;\n    this.bitrate = bitrate;\n    this.codecs = codecs;\n    this.metadata = metadata;\n    // Container specific.\n    this.containerMimeType = containerMimeType;\n    // Elementary stream specific.\n    this.sampleMimeType = sampleMimeType;\n    this.maxInputSize = maxInputSize;\n    this.initializationData =\n        initializationData == null ? Collections.emptyList() : initializationData;\n    this.drmInitData = drmInitData;\n    this.subsampleOffsetUs = subsampleOffsetUs;\n    // Video specific.\n    this.width = width;\n    this.height = height;\n    this.frameRate = frameRate;\n    this.rotationDegrees = rotationDegrees == Format.NO_VALUE ? 0 : rotationDegrees;\n    this.pixelWidthHeightRatio =\n        pixelWidthHeightRatio == Format.NO_VALUE ? 1 : pixelWidthHeightRatio;\n    this.projectionData = projectionData;\n    this.stereoMode = stereoMode;\n    this.colorInfo = colorInfo;\n    // Audio specific.\n    this.channelCount = channelCount;\n    this.sampleRate = sampleRate;\n    this.pcmEncoding = pcmEncoding;\n    this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;\n    this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;\n    // Audio and text specific.\n    this.language = Util.normalizeLanguageCode(language);\n    this.accessibilityChannel = accessibilityChannel;\n    // Provided by source.\n    this.exoMediaCryptoType = exoMediaCryptoType;\n  }\n\n  @SuppressWarnings(\"ResourceType\")\n  /* package */ Format(Parcel in) {\n    id = in.readString();\n    label = in.readString();\n    selectionFlags = in.readInt();\n    roleFlags = in.readInt();\n    bitrate = in.readInt();\n    codecs = in.readString();\n    metadata = in.readParcelable(Metadata.class.getClassLoader());\n    // Container specific.\n    containerMimeType = in.readString();\n    // Elementary stream specific.\n    sampleMimeType = in.readString();\n    maxInputSize = in.readInt();\n    int initializationDataSize = in.readInt();\n    initializationData = new ArrayList<>(initializationDataSize);\n    for (int i = 0; i < initializationDataSize; i++) {\n      initializationData.add(in.createByteArray());\n    }\n    drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());\n    subsampleOffsetUs = in.readLong();\n    // Video specific.\n    width = in.readInt();\n    height = in.readInt();\n    frameRate = in.readFloat();\n    rotationDegrees = in.readInt();\n    pixelWidthHeightRatio = in.readFloat();\n    boolean hasProjectionData = Util.readBoolean(in);\n    projectionData = hasProjectionData ? in.createByteArray() : null;\n    stereoMode = in.readInt();\n    colorInfo = in.readParcelable(ColorInfo.class.getClassLoader());\n    // Audio specific.\n    channelCount = in.readInt();\n    sampleRate = in.readInt();\n    pcmEncoding = in.readInt();\n    encoderDelay = in.readInt();\n    encoderPadding = in.readInt();\n    // Audio and text specific.\n    language = in.readString();\n    accessibilityChannel = in.readInt();\n    // Provided by source.\n    exoMediaCryptoType = null;\n  }\n\n  public Format copyWithMaxInputSize(int maxInputSize) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithLabel(@Nullable String label) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithContainerInfo(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String sampleMimeType,\n      @Nullable String codecs,\n      @Nullable Metadata metadata,\n      int bitrate,\n      int width,\n      int height,\n      int channelCount,\n      @C.SelectionFlags int selectionFlags,\n      @Nullable String language) {\n\n    if (this.metadata != null) {\n      metadata = this.metadata.copyWithAppendedEntriesFrom(metadata);\n    }\n\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  public Format copyWithManifestFormatInfo(Format manifestFormat) {\n    if (this == manifestFormat) {\n      // No need to copy from ourselves.\n      return this;\n    }\n\n    int trackType = MimeTypes.getTrackType(sampleMimeType);\n\n    // Use manifest value only.\n    String id = manifestFormat.id;\n\n    // Prefer manifest values, but fill in from sample format if missing.\n    String label = manifestFormat.label != null ? manifestFormat.label : this.label;\n    String language = this.language;\n    if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO)\n        && manifestFormat.language != null) {\n      language = manifestFormat.language;\n    }\n\n    // Prefer sample format values, but fill in from manifest if missing.\n    int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;\n    String codecs = this.codecs;\n    if (codecs == null) {\n      // The manifest format may be muxed, so filter only codecs of this format's type. If we still\n      // have more than one codec then we're unable to uniquely identify which codec to fill in.\n      String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType);\n      if (Util.splitCodecs(codecsOfType).length == 1) {\n        codecs = codecsOfType;\n      }\n    }\n\n    Metadata metadata =\n        this.metadata == null\n            ? manifestFormat.metadata\n            : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata);\n\n    float frameRate = this.frameRate;\n    if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) {\n      frameRate = manifestFormat.frameRate;\n    }\n\n    // Merge manifest and sample format values.\n    @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;\n    @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags;\n    DrmInitData drmInitData =\n        DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData);\n\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithFrameRate(float frameRate) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {\n    return copyWithAdjustments(drmInitData, metadata);\n  }\n\n  public Format copyWithMetadata(@Nullable Metadata metadata) {\n    return copyWithAdjustments(drmInitData, metadata);\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  public Format copyWithAdjustments(\n      @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) {\n    if (drmInitData == this.drmInitData && metadata == this.metadata) {\n      return this;\n    }\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithRotationDegrees(int rotationDegrees) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithBitrate(int bitrate) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithVideoSize(int width, int height) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  public Format copyWithExoMediaCryptoType(\n      @Nullable Class<? extends ExoMediaCrypto> exoMediaCryptoType) {\n    return new Format(\n        id,\n        label,\n        selectionFlags,\n        roleFlags,\n        bitrate,\n        codecs,\n        metadata,\n        containerMimeType,\n        sampleMimeType,\n        maxInputSize,\n        initializationData,\n        drmInitData,\n        subsampleOffsetUs,\n        width,\n        height,\n        frameRate,\n        rotationDegrees,\n        pixelWidthHeightRatio,\n        projectionData,\n        stereoMode,\n        colorInfo,\n        channelCount,\n        sampleRate,\n        pcmEncoding,\n        encoderDelay,\n        encoderPadding,\n        language,\n        accessibilityChannel,\n        exoMediaCryptoType);\n  }\n\n  /**\n   * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}\n   * are known, or {@link #NO_VALUE} otherwise\n   */\n  public int getPixelCount() {\n    return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height);\n  }\n\n  @Override\n  public String toString() {\n    return \"Format(\"\n        + id\n        + \", \"\n        + label\n        + \", \"\n        + containerMimeType\n        + \", \"\n        + sampleMimeType\n        + \", \"\n        + codecs\n        + \", \"\n        + bitrate\n        + \", \"\n        + language\n        + \", [\"\n        + width\n        + \", \"\n        + height\n        + \", \"\n        + frameRate\n        + \"]\"\n        + \", [\"\n        + channelCount\n        + \", \"\n        + sampleRate\n        + \"])\";\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      // Some fields for which hashing is expensive are deliberately omitted.\n      int result = 17;\n      result = 31 * result + (id == null ? 0 : id.hashCode());\n      result = 31 * result + (label != null ? label.hashCode() : 0);\n      result = 31 * result + selectionFlags;\n      result = 31 * result + roleFlags;\n      result = 31 * result + bitrate;\n      result = 31 * result + (codecs == null ? 0 : codecs.hashCode());\n      result = 31 * result + (metadata == null ? 0 : metadata.hashCode());\n      // Container specific.\n      result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());\n      // Elementary stream specific.\n      result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());\n      result = 31 * result + maxInputSize;\n      // [Omitted] initializationData.\n      // [Omitted] drmInitData.\n      result = 31 * result + (int) subsampleOffsetUs;\n      // Video specific.\n      result = 31 * result + width;\n      result = 31 * result + height;\n      result = 31 * result + Float.floatToIntBits(frameRate);\n      result = 31 * result + rotationDegrees;\n      result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio);\n      // [Omitted] projectionData.\n      result = 31 * result + stereoMode;\n      // [Omitted] colorInfo.\n      // Audio specific.\n      result = 31 * result + channelCount;\n      result = 31 * result + sampleRate;\n      result = 31 * result + pcmEncoding;\n      result = 31 * result + encoderDelay;\n      result = 31 * result + encoderPadding;\n      // Audio and text specific.\n      result = 31 * result + (language == null ? 0 : language.hashCode());\n      result = 31 * result + accessibilityChannel;\n      // Provided by source.\n      result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode());\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    Format other = (Format) obj;\n    if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) {\n      return false;\n    }\n    // Field equality checks ordered by type, with the cheapest checks first.\n    return selectionFlags == other.selectionFlags\n        && roleFlags == other.roleFlags\n        && bitrate == other.bitrate\n        && maxInputSize == other.maxInputSize\n        && subsampleOffsetUs == other.subsampleOffsetUs\n        && width == other.width\n        && height == other.height\n        && rotationDegrees == other.rotationDegrees\n        && stereoMode == other.stereoMode\n        && channelCount == other.channelCount\n        && sampleRate == other.sampleRate\n        && pcmEncoding == other.pcmEncoding\n        && encoderDelay == other.encoderDelay\n        && encoderPadding == other.encoderPadding\n        && accessibilityChannel == other.accessibilityChannel\n        && Float.compare(frameRate, other.frameRate) == 0\n        && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0\n        && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType)\n        && Util.areEqual(id, other.id)\n        && Util.areEqual(label, other.label)\n        && Util.areEqual(codecs, other.codecs)\n        && Util.areEqual(containerMimeType, other.containerMimeType)\n        && Util.areEqual(sampleMimeType, other.sampleMimeType)\n        && Util.areEqual(language, other.language)\n        && Arrays.equals(projectionData, other.projectionData)\n        && Util.areEqual(metadata, other.metadata)\n        && Util.areEqual(colorInfo, other.colorInfo)\n        && Util.areEqual(drmInitData, other.drmInitData)\n        && initializationDataEquals(other);\n  }\n\n  /**\n   * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are\n   * equal.\n   *\n   * @param other The other format whose {@link #initializationData} is being compared.\n   * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are\n   *     equal.\n   */\n  public boolean initializationDataEquals(Format other) {\n    if (initializationData.size() != other.initializationData.size()) {\n      return false;\n    }\n    for (int i = 0; i < initializationData.size(); i++) {\n      if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  // Utility methods\n\n  /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */\n  public static String toLogString(@Nullable Format format) {\n    if (format == null) {\n      return \"null\";\n    }\n    StringBuilder builder = new StringBuilder();\n    builder.append(\"id=\").append(format.id).append(\", mimeType=\").append(format.sampleMimeType);\n    if (format.bitrate != Format.NO_VALUE) {\n      builder.append(\", bitrate=\").append(format.bitrate);\n    }\n    if (format.codecs != null) {\n      builder.append(\", codecs=\").append(format.codecs);\n    }\n    if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {\n      builder.append(\", res=\").append(format.width).append(\"x\").append(format.height);\n    }\n    if (format.frameRate != Format.NO_VALUE) {\n      builder.append(\", fps=\").append(format.frameRate);\n    }\n    if (format.channelCount != Format.NO_VALUE) {\n      builder.append(\", channels=\").append(format.channelCount);\n    }\n    if (format.sampleRate != Format.NO_VALUE) {\n      builder.append(\", sample_rate=\").append(format.sampleRate);\n    }\n    if (format.language != null) {\n      builder.append(\", language=\").append(format.language);\n    }\n    if (format.label != null) {\n      builder.append(\", label=\").append(format.label);\n    }\n    return builder.toString();\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(label);\n    dest.writeInt(selectionFlags);\n    dest.writeInt(roleFlags);\n    dest.writeInt(bitrate);\n    dest.writeString(codecs);\n    dest.writeParcelable(metadata, 0);\n    // Container specific.\n    dest.writeString(containerMimeType);\n    // Elementary stream specific.\n    dest.writeString(sampleMimeType);\n    dest.writeInt(maxInputSize);\n    int initializationDataSize = initializationData.size();\n    dest.writeInt(initializationDataSize);\n    for (int i = 0; i < initializationDataSize; i++) {\n      dest.writeByteArray(initializationData.get(i));\n    }\n    dest.writeParcelable(drmInitData, 0);\n    dest.writeLong(subsampleOffsetUs);\n    // Video specific.\n    dest.writeInt(width);\n    dest.writeInt(height);\n    dest.writeFloat(frameRate);\n    dest.writeInt(rotationDegrees);\n    dest.writeFloat(pixelWidthHeightRatio);\n    Util.writeBoolean(dest, projectionData != null);\n    if (projectionData != null) {\n      dest.writeByteArray(projectionData);\n    }\n    dest.writeInt(stereoMode);\n    dest.writeParcelable(colorInfo, flags);\n    // Audio specific.\n    dest.writeInt(channelCount);\n    dest.writeInt(sampleRate);\n    dest.writeInt(pcmEncoding);\n    dest.writeInt(encoderDelay);\n    dest.writeInt(encoderPadding);\n    // Audio and text specific.\n    dest.writeString(language);\n    dest.writeInt(accessibilityChannel);\n  }\n\n  public static final Creator<Format> CREATOR = new Creator<Format>() {\n\n    @Override\n    public Format createFromParcel(Parcel in) {\n      return new Format(in);\n    }\n\n    @Override\n    public Format[] newArray(int size) {\n      return new Format[size];\n    }\n\n  };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/FormatHolder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.drm.DrmSession;\n\n/**\n * Holds a {@link Format}.\n */\npublic final class FormatHolder {\n\n  /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */\n  // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal\n  // ref: b/129764794].\n  public boolean includesDrmSession;\n\n  /** An accompanying context for decrypting samples in the format. */\n  @Nullable public DrmSession<?> drmSession;\n\n  /** The held {@link Format}. */\n  @Nullable public Format format;\n\n  /** Clears the holder. */\n  public void clear() {\n    includesDrmSession = false;\n    drmSession = null;\n    format = null;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\n/**\n * Thrown when an attempt is made to seek to a position that does not exist in the player's\n * {@link Timeline}.\n */\npublic final class IllegalSeekPositionException extends IllegalStateException {\n\n  /**\n   * The {@link Timeline} in which the seek was attempted.\n   */\n  public final Timeline timeline;\n  /**\n   * The index of the window being seeked to.\n   */\n  public final int windowIndex;\n  /**\n   * The seek position in the specified window.\n   */\n  public final long positionMs;\n\n  /**\n   * @param timeline The {@link Timeline} in which the seek was attempted.\n   * @param windowIndex The index of the window being seeked to.\n   * @param positionMs The seek position in the specified window.\n   */\n  public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {\n    this.timeline = timeline;\n    this.windowIndex = windowIndex;\n    this.positionMs = positionMs;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/LoadControl.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.upstream.Allocator;\n\n/**\n * Controls buffering of media.\n */\npublic interface LoadControl {\n\n  /**\n   * Called by the player when prepared with a new source.\n   */\n  void onPrepared();\n\n  /**\n   * Called by the player when a track selection occurs.\n   *\n   * @param renderers The renderers.\n   * @param trackGroups The {@link TrackGroup}s from which the selection was made.\n   * @param trackSelections The track selections that were made.\n   */\n  void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,\n                        TrackSelectionArray trackSelections);\n\n  /**\n   * Called by the player when stopped.\n   */\n  void onStopped();\n\n  /**\n   * Called by the player when released.\n   */\n  void onReleased();\n\n  /**\n   * Returns the {@link Allocator} that should be used to obtain media buffer allocations.\n   */\n  Allocator getAllocator();\n\n  /**\n   * Returns the duration of media to retain in the buffer prior to the current playback position,\n   * for fast backward seeking.\n   * <p>\n   * Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer will\n   * only be fast if the back-buffer contains a keyframe prior to the seek position.\n   * <p>\n   * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not\n   * currently supported.\n   *\n   * @return The duration of media to retain in the buffer prior to the current playback position,\n   *     in microseconds.\n   */\n  long getBackBufferDurationUs();\n\n  /**\n   * Returns whether media should be retained from the keyframe before the current playback position\n   * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.\n   * <p>\n   * Warning: Returning true will cause the back-buffer size to depend on the spacing of keyframes\n   * in the media being played. Returning true is not recommended unless you control the media and\n   * are comfortable with the back-buffer size exceeding {@link #getBackBufferDurationUs()} by as\n   * much as the maximum duration between adjacent keyframes in the media.\n   * <p>\n   * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not\n   * currently supported.\n   *\n   * @return Whether media should be retained from the keyframe before the current playback position\n   * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.\n   */\n  boolean retainBackBufferFromKeyframe();\n\n  /**\n   * Called by the player to determine whether it should continue to load the source.\n   *\n   * @param bufferedDurationUs The duration of media that's currently buffered.\n   * @param playbackSpeed The current playback speed.\n   * @return Whether the loading should continue.\n   */\n  boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed);\n\n  /**\n   * Called repeatedly by the player when it's loading the source, has yet to start playback, and\n   * has the minimum amount of data necessary for playback to be started. The value returned\n   * determines whether playback is actually started. The load control may opt to return {@code\n   * false} until some condition has been met (e.g. a certain amount of media is buffered).\n   *\n   * @param bufferedDurationUs The duration of media that's currently buffered.\n   * @param playbackSpeed The current playback speed.\n   * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by\n   *     buffer depletion rather than a user action. Hence this parameter is false during initial\n   *     buffering and when buffering as a result of a seek operation.\n   * @return Whether playback should be allowed to start or resume.\n   */\n  boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.ClippingMediaPeriod;\nimport com.google.android.exoplayer2.source.EmptySampleStream;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */\n/* package */ final class MediaPeriodHolder {\n\n  private static final String TAG = \"MediaPeriodHolder\";\n\n  /** The {@link MediaPeriod} wrapped by this class. */\n  public final MediaPeriod mediaPeriod;\n  /** The unique timeline period identifier the media period belongs to. */\n  public final Object uid;\n  /**\n   * The sample streams for each renderer associated with this period. May contain null elements.\n   */\n  public final @NullableType SampleStream[] sampleStreams;\n\n  /** Whether the media period has finished preparing. */\n  public boolean prepared;\n  /** Whether any of the tracks of this media period are enabled. */\n  public boolean hasEnabledTracks;\n  /** {@link MediaPeriodInfo} about this media period. */\n  public MediaPeriodInfo info;\n\n  private final boolean[] mayRetainStreamFlags;\n  private final RendererCapabilities[] rendererCapabilities;\n  private final TrackSelector trackSelector;\n  private final MediaSource mediaSource;\n\n  @Nullable private MediaPeriodHolder next;\n  private TrackGroupArray trackGroups;\n  private TrackSelectorResult trackSelectorResult;\n  private long rendererPositionOffsetUs;\n\n  /**\n   * Creates a new holder with information required to play it as part of a timeline.\n   *\n   * @param rendererCapabilities The renderer capabilities.\n   * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.\n   * @param trackSelector The track selector.\n   * @param allocator The allocator.\n   * @param mediaSource The media source that produced the media period.\n   * @param info Information used to identify this media period in its timeline period.\n   * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each\n   *     renderer.\n   */\n  public MediaPeriodHolder(\n      RendererCapabilities[] rendererCapabilities,\n      long rendererPositionOffsetUs,\n      TrackSelector trackSelector,\n      Allocator allocator,\n      MediaSource mediaSource,\n      MediaPeriodInfo info,\n      TrackSelectorResult emptyTrackSelectorResult) {\n    this.rendererCapabilities = rendererCapabilities;\n    this.rendererPositionOffsetUs = rendererPositionOffsetUs;\n    this.trackSelector = trackSelector;\n    this.mediaSource = mediaSource;\n    this.uid = info.id.periodUid;\n    this.info = info;\n    this.trackGroups = TrackGroupArray.EMPTY;\n    this.trackSelectorResult = emptyTrackSelectorResult;\n    sampleStreams = new SampleStream[rendererCapabilities.length];\n    mayRetainStreamFlags = new boolean[rendererCapabilities.length];\n    mediaPeriod =\n        createMediaPeriod(\n            info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);\n  }\n\n  /**\n   * Converts time relative to the start of the period to the respective renderer time using {@link\n   * #getRendererOffset()}, in microseconds.\n   */\n  public long toRendererTime(long periodTimeUs) {\n    return periodTimeUs + getRendererOffset();\n  }\n\n  /**\n   * Converts renderer time to the respective time relative to the start of the period using {@link\n   * #getRendererOffset()}, in microseconds.\n   */\n  public long toPeriodTime(long rendererTimeUs) {\n    return rendererTimeUs - getRendererOffset();\n  }\n\n  /** Returns the renderer time of the start of the period, in microseconds. */\n  public long getRendererOffset() {\n    return rendererPositionOffsetUs;\n  }\n\n  /**\n   * Sets the renderer time of the start of the period, in microseconds.\n   *\n   * @param rendererPositionOffsetUs The new renderer position offset, in microseconds.\n   */\n  public void setRendererOffset(long rendererPositionOffsetUs) {\n    this.rendererPositionOffsetUs = rendererPositionOffsetUs;\n  }\n\n  /** Returns start position of period in renderer time. */\n  public long getStartPositionRendererTime() {\n    return info.startPositionUs + rendererPositionOffsetUs;\n  }\n\n  /** Returns whether the period is fully buffered. */\n  public boolean isFullyBuffered() {\n    return prepared\n        && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);\n  }\n\n  /**\n   * Returns the buffered position in microseconds. If the period is buffered to the end, then the\n   * period duration is returned.\n   *\n   * @return The buffered position in microseconds.\n   */\n  public long getBufferedPositionUs() {\n    if (!prepared) {\n      return info.startPositionUs;\n    }\n    long bufferedPositionUs =\n        hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE;\n    return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs;\n  }\n\n  /**\n   * Returns the next load time relative to the start of the period, or {@link C#TIME_END_OF_SOURCE}\n   * if loading has finished.\n   */\n  public long getNextLoadPositionUs() {\n    return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();\n  }\n\n  /**\n   * Handles period preparation.\n   *\n   * @param playbackSpeed The current playback speed.\n   * @param timeline The current {@link Timeline}.\n   * @throws ExoPlaybackException If an error occurs during track selection.\n   */\n  public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException {\n    prepared = true;\n    trackGroups = mediaPeriod.getTrackGroups();\n    TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline);\n    long newStartPositionUs =\n        applyTrackSelection(\n            selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false);\n    rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs;\n    info = info.copyWithStartPositionUs(newStartPositionUs);\n  }\n\n  /**\n   * Reevaluates the buffer of the media period at the given renderer position. Should only be\n   * called if this is the loading media period.\n   *\n   * @param rendererPositionUs The playing position in renderer time, in microseconds.\n   */\n  public void reevaluateBuffer(long rendererPositionUs) {\n    Assertions.checkState(isLoadingMediaPeriod());\n    if (prepared) {\n      mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs));\n    }\n  }\n\n  /**\n   * Continues loading the media period at the given renderer position. Should only be called if\n   * this is the loading media period.\n   *\n   * @param rendererPositionUs The load position in renderer time, in microseconds.\n   */\n  public void continueLoading(long rendererPositionUs) {\n    Assertions.checkState(isLoadingMediaPeriod());\n    long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);\n    mediaPeriod.continueLoading(loadingPeriodPositionUs);\n  }\n\n  /**\n   * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}.\n   *\n   * <p>The new track selection needs to be applied with {@link\n   * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect.\n   *\n   * @param playbackSpeed The current playback speed.\n   * @param timeline The current {@link Timeline}.\n   * @return The {@link TrackSelectorResult}.\n   * @throws ExoPlaybackException If an error occurs during track selection.\n   */\n  public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline)\n      throws ExoPlaybackException {\n    TrackSelectorResult selectorResult =\n        trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline);\n    for (TrackSelection trackSelection : selectorResult.selections.getAll()) {\n      if (trackSelection != null) {\n        trackSelection.onPlaybackSpeed(playbackSpeed);\n      }\n    }\n    return selectorResult;\n  }\n\n  /**\n   * Applies a {@link TrackSelectorResult} to the period.\n   *\n   * @param trackSelectorResult The {@link TrackSelectorResult} to apply.\n   * @param positionUs The position relative to the start of the period at which to apply the new\n   *     track selections, in microseconds.\n   * @param forceRecreateStreams Whether all streams are forced to be recreated.\n   * @return The actual position relative to the start of the period at which the new track\n   *     selections are applied.\n   */\n  public long applyTrackSelection(\n      TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams) {\n    return applyTrackSelection(\n        trackSelectorResult,\n        positionUs,\n        forceRecreateStreams,\n        new boolean[rendererCapabilities.length]);\n  }\n\n  /**\n   * Applies a {@link TrackSelectorResult} to the period.\n   *\n   * @param newTrackSelectorResult The {@link TrackSelectorResult} to apply.\n   * @param positionUs The position relative to the start of the period at which to apply the new\n   *     track selections, in microseconds.\n   * @param forceRecreateStreams Whether all streams are forced to be recreated.\n   * @param streamResetFlags Will be populated to indicate which streams have been reset or were\n   *     newly created.\n   * @return The actual position relative to the start of the period at which the new track\n   *     selections are applied.\n   */\n  public long applyTrackSelection(\n      TrackSelectorResult newTrackSelectorResult,\n      long positionUs,\n      boolean forceRecreateStreams,\n      boolean[] streamResetFlags) {\n    for (int i = 0; i < newTrackSelectorResult.length; i++) {\n      mayRetainStreamFlags[i] =\n          !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i);\n    }\n\n    // Undo the effect of previous call to associate no-sample renderers with empty tracks\n    // so the mediaPeriod receives back whatever it sent us before.\n    disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);\n    disableTrackSelectionsInResult();\n    trackSelectorResult = newTrackSelectorResult;\n    enableTrackSelectionsInResult();\n    // Disable streams on the period and get new streams for updated/newly-enabled tracks.\n    TrackSelectionArray trackSelections = newTrackSelectorResult.selections;\n    positionUs =\n        mediaPeriod.selectTracks(\n            trackSelections.getAll(),\n            mayRetainStreamFlags,\n            sampleStreams,\n            streamResetFlags,\n            positionUs);\n    associateNoSampleRenderersWithEmptySampleStream(sampleStreams);\n\n    // Update whether we have enabled tracks and sanity check the expected streams are non-null.\n    hasEnabledTracks = false;\n    for (int i = 0; i < sampleStreams.length; i++) {\n      if (sampleStreams[i] != null) {\n        Assertions.checkState(newTrackSelectorResult.isRendererEnabled(i));\n        // hasEnabledTracks should be true only when non-empty streams exists.\n        if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) {\n          hasEnabledTracks = true;\n        }\n      } else {\n        Assertions.checkState(trackSelections.get(i) == null);\n      }\n    }\n    return positionUs;\n  }\n\n  /** Releases the media period. No other method should be called after the release. */\n  public void release() {\n    disableTrackSelectionsInResult();\n    releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);\n  }\n\n  /**\n   * Sets the next media period holder in the queue.\n   *\n   * @param nextMediaPeriodHolder The next holder, or null if this will be the new loading media\n   *     period holder at the end of the queue.\n   */\n  public void setNext(@Nullable MediaPeriodHolder nextMediaPeriodHolder) {\n    if (nextMediaPeriodHolder == next) {\n      return;\n    }\n    disableTrackSelectionsInResult();\n    next = nextMediaPeriodHolder;\n    enableTrackSelectionsInResult();\n  }\n\n  /**\n   * Returns the next media period holder in the queue, or null if this is the last media period\n   * (and thus the loading media period).\n   */\n  @Nullable\n  public MediaPeriodHolder getNext() {\n    return next;\n  }\n\n  /** Returns the {@link TrackGroupArray} exposed by this media period. */\n  public TrackGroupArray getTrackGroups() {\n    return trackGroups;\n  }\n\n  /** Returns the {@link TrackSelectorResult} which is currently applied. */\n  public TrackSelectorResult getTrackSelectorResult() {\n    return trackSelectorResult;\n  }\n\n  private void enableTrackSelectionsInResult() {\n    if (!isLoadingMediaPeriod()) {\n      return;\n    }\n    for (int i = 0; i < trackSelectorResult.length; i++) {\n      boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);\n      TrackSelection trackSelection = trackSelectorResult.selections.get(i);\n      if (rendererEnabled && trackSelection != null) {\n        trackSelection.enable();\n      }\n    }\n  }\n\n  private void disableTrackSelectionsInResult() {\n    if (!isLoadingMediaPeriod()) {\n      return;\n    }\n    for (int i = 0; i < trackSelectorResult.length; i++) {\n      boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);\n      TrackSelection trackSelection = trackSelectorResult.selections.get(i);\n      if (rendererEnabled && trackSelection != null) {\n        trackSelection.disable();\n      }\n    }\n  }\n\n  /**\n   * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@link\n   * EmptySampleStream} that was associated with it.\n   */\n  private void disassociateNoSampleRenderersWithEmptySampleStream(\n      @NullableType SampleStream[] sampleStreams) {\n    for (int i = 0; i < rendererCapabilities.length; i++) {\n      if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) {\n        sampleStreams[i] = null;\n      }\n    }\n  }\n\n  /**\n   * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with\n   * a dummy {@link EmptySampleStream}.\n   */\n  private void associateNoSampleRenderersWithEmptySampleStream(\n      @NullableType SampleStream[] sampleStreams) {\n    for (int i = 0; i < rendererCapabilities.length; i++) {\n      if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE\n          && trackSelectorResult.isRendererEnabled(i)) {\n        sampleStreams[i] = new EmptySampleStream();\n      }\n    }\n  }\n\n  private boolean isLoadingMediaPeriod() {\n    return next == null;\n  }\n\n  /** Returns a media period corresponding to the given {@code id}. */\n  private static MediaPeriod createMediaPeriod(\n      MediaPeriodId id,\n      MediaSource mediaSource,\n      Allocator allocator,\n      long startPositionUs,\n      long endPositionUs) {\n    MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);\n    if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {\n      mediaPeriod =\n          new ClippingMediaPeriod(\n              mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);\n    }\n    return mediaPeriod;\n  }\n\n  /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */\n  private static void releaseMediaPeriod(\n      long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {\n    try {\n      if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {\n        mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);\n      } else {\n        mediaSource.releasePeriod(mediaPeriod);\n      }\n    } catch (RuntimeException e) {\n      // There's nothing we can do.\n      Log.e(TAG, \"Period release failed.\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Stores the information required to load and play a {@link MediaPeriod}. */\n/* package */ final class MediaPeriodInfo {\n\n  /** The media period's identifier. */\n  public final MediaPeriodId id;\n  /** The start position of the media to play within the media period, in microseconds. */\n  public final long startPositionUs;\n  /**\n   * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}\n   * if this is not an ad or the next content media period should be played from its default\n   * position.\n   */\n  public final long contentPositionUs;\n  /**\n   * The end position to which the media period's content is clipped in order to play a following ad\n   * group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this\n   * media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad\n   * follows at the end of this content media period.\n   */\n  public final long endPositionUs;\n  /**\n   * The duration of the media period, like {@link #endPositionUs} but with {@link\n   * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if\n   * known.\n   */\n  public final long durationUs;\n  /**\n   * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media\n   * period corresponding to a timeline period without ads).\n   */\n  public final boolean isLastInTimelinePeriod;\n  /**\n   * Whether this is the last media period in the entire timeline. If true, {@link\n   * #isLastInTimelinePeriod} will also be true.\n   */\n  public final boolean isFinal;\n\n  MediaPeriodInfo(\n      MediaPeriodId id,\n      long startPositionUs,\n      long contentPositionUs,\n      long endPositionUs,\n      long durationUs,\n      boolean isLastInTimelinePeriod,\n      boolean isFinal) {\n    this.id = id;\n    this.startPositionUs = startPositionUs;\n    this.contentPositionUs = contentPositionUs;\n    this.endPositionUs = endPositionUs;\n    this.durationUs = durationUs;\n    this.isLastInTimelinePeriod = isLastInTimelinePeriod;\n    this.isFinal = isFinal;\n  }\n\n  /**\n   * Returns a copy of this instance with the start position set to the specified value. May return\n   * the same instance if nothing changed.\n   */\n  public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {\n    return startPositionUs == this.startPositionUs\n        ? this\n        : new MediaPeriodInfo(\n            id,\n            startPositionUs,\n            contentPositionUs,\n            endPositionUs,\n            durationUs,\n            isLastInTimelinePeriod,\n            isFinal);\n  }\n\n  /**\n   * Returns a copy of this instance with the content position set to the specified value. May\n   * return the same instance if nothing changed.\n   */\n  public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) {\n    return contentPositionUs == this.contentPositionUs\n        ? this\n        : new MediaPeriodInfo(\n            id,\n            startPositionUs,\n            contentPositionUs,\n            endPositionUs,\n            durationUs,\n            isLastInTimelinePeriod,\n            isFinal);\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    MediaPeriodInfo that = (MediaPeriodInfo) o;\n    return startPositionUs == that.startPositionUs\n        && contentPositionUs == that.contentPositionUs\n        && endPositionUs == that.endPositionUs\n        && durationUs == that.durationUs\n        && isLastInTimelinePeriod == that.isLastInTimelinePeriod\n        && isFinal == that.isFinal\n        && Util.areEqual(id, that.id);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + id.hashCode();\n    result = 31 * result + (int) startPositionUs;\n    result = 31 * result + (int) contentPositionUs;\n    result = 31 * result + (int) endPositionUs;\n    result = 31 * result + (int) durationUs;\n    result = 31 * result + (isLastInTimelinePeriod ? 1 : 0);\n    result = 31 * result + (isFinal ? 1 : 0);\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Player.RepeatMode;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Holds a queue of media periods, from the currently playing media period at the front to the\n * loading media period at the end of the queue, with methods for controlling loading and updating\n * the queue. Also has a reference to the media period currently being read.\n */\n/* package */ final class MediaPeriodQueue {\n\n  /**\n   * Limits the maximum number of periods to buffer ahead of the current playing period. The\n   * buffering policy normally prevents buffering too far ahead, but the policy could allow too many\n   * small periods to be buffered if the period count were not limited.\n   */\n  private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;\n\n  private final Timeline.Period period;\n  private final Timeline.Window window;\n\n  private long nextWindowSequenceNumber;\n  private Timeline timeline;\n  private @RepeatMode int repeatMode;\n  private boolean shuffleModeEnabled;\n  @Nullable private MediaPeriodHolder playing;\n  @Nullable private MediaPeriodHolder reading;\n  @Nullable private MediaPeriodHolder loading;\n  private int length;\n  @Nullable private Object oldFrontPeriodUid;\n  private long oldFrontPeriodWindowSequenceNumber;\n\n  /** Creates a new media period queue. */\n  public MediaPeriodQueue() {\n    period = new Timeline.Period();\n    window = new Timeline.Window();\n    timeline = Timeline.EMPTY;\n  }\n\n  /**\n   * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued\n   * media periods to take into account the new timeline.\n   */\n  public void setTimeline(Timeline timeline) {\n    this.timeline = timeline;\n  }\n\n  /**\n   * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled.\n   * If not, it is necessary to seek to the current playback position.\n   */\n  public boolean updateRepeatMode(@RepeatMode int repeatMode) {\n    this.repeatMode = repeatMode;\n    return updateForPlaybackModeChange();\n  }\n\n  /**\n   * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully\n   * handled. If not, it is necessary to seek to the current playback position.\n   */\n  public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) {\n    this.shuffleModeEnabled = shuffleModeEnabled;\n    return updateForPlaybackModeChange();\n  }\n\n  /** Returns whether {@code mediaPeriod} is the current loading media period. */\n  public boolean isLoading(MediaPeriod mediaPeriod) {\n    return loading != null && loading.mediaPeriod == mediaPeriod;\n  }\n\n  /**\n   * If there is a loading period, reevaluates its buffer.\n   *\n   * @param rendererPositionUs The current renderer position.\n   */\n  public void reevaluateBuffer(long rendererPositionUs) {\n    if (loading != null) {\n      loading.reevaluateBuffer(rendererPositionUs);\n    }\n  }\n\n  /** Returns whether a new loading media period should be enqueued, if available. */\n  public boolean shouldLoadNextMediaPeriod() {\n    return loading == null\n        || (!loading.info.isFinal\n            && loading.isFullyBuffered()\n            && loading.info.durationUs != C.TIME_UNSET\n            && length < MAXIMUM_BUFFER_AHEAD_PERIODS);\n  }\n\n  /**\n   * Returns the {@link MediaPeriodInfo} for the next media period to load.\n   *\n   * @param rendererPositionUs The current renderer position.\n   * @param playbackInfo The current playback information.\n   * @return The {@link MediaPeriodInfo} for the next media period to load, or {@code null} if not\n   *     yet known.\n   */\n  public @Nullable MediaPeriodInfo getNextMediaPeriodInfo(\n      long rendererPositionUs, PlaybackInfo playbackInfo) {\n    return loading == null\n        ? getFirstMediaPeriodInfo(playbackInfo)\n        : getFollowingMediaPeriodInfo(loading, rendererPositionUs);\n  }\n\n  /**\n   * Enqueues a new media period holder based on the specified information as the new loading media\n   * period, and returns it.\n   *\n   * @param rendererCapabilities The renderer capabilities.\n   * @param trackSelector The track selector.\n   * @param allocator The allocator.\n   * @param mediaSource The media source that produced the media period.\n   * @param info Information used to identify this media period in its timeline period.\n   * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each\n   *     renderer.\n   */\n  public MediaPeriodHolder enqueueNextMediaPeriodHolder(\n      RendererCapabilities[] rendererCapabilities,\n      TrackSelector trackSelector,\n      Allocator allocator,\n      MediaSource mediaSource,\n      MediaPeriodInfo info,\n      TrackSelectorResult emptyTrackSelectorResult) {\n    long rendererPositionOffsetUs =\n        loading == null\n            ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET\n                ? info.contentPositionUs\n                : 0)\n            : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);\n    MediaPeriodHolder newPeriodHolder =\n        new MediaPeriodHolder(\n            rendererCapabilities,\n            rendererPositionOffsetUs,\n            trackSelector,\n            allocator,\n            mediaSource,\n            info,\n            emptyTrackSelectorResult);\n    if (loading != null) {\n      loading.setNext(newPeriodHolder);\n    } else {\n      playing = newPeriodHolder;\n      reading = newPeriodHolder;\n    }\n    oldFrontPeriodUid = null;\n    loading = newPeriodHolder;\n    length++;\n    return newPeriodHolder;\n  }\n\n  /**\n   * Returns the loading period holder which is at the end of the queue, or null if the queue is\n   * empty.\n   */\n  @Nullable\n  public MediaPeriodHolder getLoadingPeriod() {\n    return loading;\n  }\n\n  /**\n   * Returns the playing period holder which is at the front of the queue, or null if the queue is\n   * empty.\n   */\n  @Nullable\n  public MediaPeriodHolder getPlayingPeriod() {\n    return playing;\n  }\n\n  /** Returns the reading period holder, or null if the queue is empty. */\n  @Nullable\n  public MediaPeriodHolder getReadingPeriod() {\n    return reading;\n  }\n\n  /**\n   * Continues reading from the next period holder in the queue.\n   *\n   * @return The updated reading period holder.\n   */\n  public MediaPeriodHolder advanceReadingPeriod() {\n    Assertions.checkState(reading != null && reading.getNext() != null);\n    reading = reading.getNext();\n    return reading;\n  }\n\n  /**\n   * Dequeues the playing period holder from the front of the queue and advances the playing period\n   * holder to be the next item in the queue.\n   *\n   * @return The updated playing period holder, or null if the queue is or becomes empty.\n   */\n  @Nullable\n  public MediaPeriodHolder advancePlayingPeriod() {\n    if (playing == null) {\n      return null;\n    }\n    if (playing == reading) {\n      reading = playing.getNext();\n    }\n    playing.release();\n    length--;\n    if (length == 0) {\n      loading = null;\n      oldFrontPeriodUid = playing.uid;\n      oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;\n    }\n    playing = playing.getNext();\n    return playing;\n  }\n\n  /**\n   * Removes all period holders after the given period holder. This process may also remove the\n   * currently reading period holder. If that is the case, the reading period holder is set to be\n   * the same as the playing period holder at the front of the queue.\n   *\n   * @param mediaPeriodHolder The media period holder that shall be the new end of the queue.\n   * @return Whether the reading period has been removed.\n   */\n  public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) {\n    Assertions.checkState(mediaPeriodHolder != null);\n    boolean removedReading = false;\n    loading = mediaPeriodHolder;\n    while (mediaPeriodHolder.getNext() != null) {\n      mediaPeriodHolder = mediaPeriodHolder.getNext();\n      if (mediaPeriodHolder == reading) {\n        reading = playing;\n        removedReading = true;\n      }\n      mediaPeriodHolder.release();\n      length--;\n    }\n    loading.setNext(null);\n    return removedReading;\n  }\n\n  /**\n   * Clears the queue.\n   *\n   * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front\n   *     of queue (typically the playing one) for later reuse.\n   */\n  public void clear(boolean keepFrontPeriodUid) {\n    MediaPeriodHolder front = playing;\n    if (front != null) {\n      oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null;\n      oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;\n      removeAfter(front);\n      front.release();\n    } else if (!keepFrontPeriodUid) {\n      oldFrontPeriodUid = null;\n    }\n    playing = null;\n    loading = null;\n    reading = null;\n    length = 0;\n  }\n\n  /**\n   * Updates media periods in the queue to take into account the latest timeline, and returns\n   * whether the timeline change has been fully handled. If not, it is necessary to seek to the\n   * current playback position. The method assumes that the first media period in the queue is still\n   * consistent with the new timeline.\n   *\n   * @param rendererPositionUs The current renderer position in microseconds.\n   * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read\n   *     the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they\n   *     have read to the end.\n   * @return Whether the timeline change has been handled completely.\n   */\n  public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) {\n    // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline\n    // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be\n    // handled here.\n    MediaPeriodHolder previousPeriodHolder = null;\n    MediaPeriodHolder periodHolder = playing;\n    while (periodHolder != null) {\n      MediaPeriodInfo oldPeriodInfo = periodHolder.info;\n\n      // Get period info based on new timeline.\n      MediaPeriodInfo newPeriodInfo;\n      if (previousPeriodHolder == null) {\n        // The id and start position of the first period have already been verified by\n        // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline\n        // and isLastInPeriod flags.\n        newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);\n      } else {\n        newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);\n        if (newPeriodInfo == null) {\n          // We've loaded a next media period that is not in the new timeline.\n          return !removeAfter(previousPeriodHolder);\n        }\n        if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {\n          // The new media period has a different id or start position.\n          return !removeAfter(previousPeriodHolder);\n        }\n      }\n\n      // Use new period info, but keep old content position.\n      periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);\n\n      if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {\n        // The period duration changed. Remove all subsequent periods and check whether we read\n        // beyond the new duration.\n        long newDurationInRendererTime =\n            newPeriodInfo.durationUs == C.TIME_UNSET\n                ? Long.MAX_VALUE\n                : periodHolder.toRendererTime(newPeriodInfo.durationUs);\n        boolean isReadingAndReadBeyondNewDuration =\n            periodHolder == reading\n                && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE\n                    || maxRendererReadPositionUs >= newDurationInRendererTime);\n        boolean readingPeriodRemoved = removeAfter(periodHolder);\n        return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;\n      }\n\n      previousPeriodHolder = periodHolder;\n      periodHolder = periodHolder.getNext();\n    }\n    return true;\n  }\n\n  /**\n   * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into\n   * account the current timeline. This method must only be called if the period is still part of\n   * the current timeline.\n   *\n   * @param info Media period info for a media period based on an old timeline.\n   * @return The updated media period info for the current timeline.\n   */\n  public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) {\n    MediaPeriodId id = info.id;\n    boolean isLastInPeriod = isLastInPeriod(id);\n    boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);\n    timeline.getPeriodByUid(info.id.periodUid, period);\n    long durationUs =\n        id.isAd()\n            ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)\n            : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE\n                ? period.getDurationUs()\n                : info.endPositionUs);\n    return new MediaPeriodInfo(\n        id,\n        info.startPositionUs,\n        info.contentPositionUs,\n        info.endPositionUs,\n        durationUs,\n        isLastInPeriod,\n        isLastInTimeline);\n  }\n\n  /**\n   * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be\n   * played, returning an identifier for an ad group if one needs to be played before the specified\n   * position, or an identifier for a content media period if not.\n   *\n   * @param periodUid The uid of the timeline period to play.\n   * @param positionUs The next content position in the period to play.\n   * @return The identifier for the first media period to play, taking into account unplayed ads.\n   */\n  public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) {\n    long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid);\n    return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber);\n  }\n\n  // Internal methods.\n\n  /**\n   * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be\n   * played, returning an identifier for an ad group if one needs to be played before the specified\n   * position, or an identifier for a content media period if not.\n   *\n   * @param periodUid The uid of the timeline period to play.\n   * @param positionUs The next content position in the period to play.\n   * @param windowSequenceNumber The sequence number of the window in the buffered sequence of\n   *     windows this period is part of.\n   * @return The identifier for the first media period to play, taking into account unplayed ads.\n   */\n  private MediaPeriodId resolveMediaPeriodIdForAds(\n      Object periodUid, long positionUs, long windowSequenceNumber) {\n    timeline.getPeriodByUid(periodUid, period);\n    int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);\n    if (adGroupIndex == C.INDEX_UNSET) {\n      int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);\n      return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);\n    } else {\n      int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);\n      return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);\n    }\n  }\n\n  /**\n   * Resolves the specified period uid to a corresponding window sequence number. Either by reusing\n   * the window sequence number of an existing matching media period or by creating a new window\n   * sequence number.\n   *\n   * @param periodUid The uid of the timeline period.\n   * @return A window sequence number for a media period created for this timeline period.\n   */\n  private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) {\n    int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex;\n    if (oldFrontPeriodUid != null) {\n      int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid);\n      if (oldFrontPeriodIndex != C.INDEX_UNSET) {\n        int oldFrontWindowIndex = timeline.getPeriod(oldFrontPeriodIndex, period).windowIndex;\n        if (oldFrontWindowIndex == windowIndex) {\n          // Try to match old front uid after the queue has been cleared.\n          return oldFrontPeriodWindowSequenceNumber;\n        }\n      }\n    }\n    MediaPeriodHolder mediaPeriodHolder = playing;\n    while (mediaPeriodHolder != null) {\n      if (mediaPeriodHolder.uid.equals(periodUid)) {\n        // Reuse window sequence number of first exact period match.\n        return mediaPeriodHolder.info.id.windowSequenceNumber;\n      }\n      mediaPeriodHolder = mediaPeriodHolder.getNext();\n    }\n    mediaPeriodHolder = playing;\n    while (mediaPeriodHolder != null) {\n      int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid);\n      if (indexOfHolderInTimeline != C.INDEX_UNSET) {\n        int holderWindowIndex = timeline.getPeriod(indexOfHolderInTimeline, period).windowIndex;\n        if (holderWindowIndex == windowIndex) {\n          // As an alternative, try to match other periods of the same window.\n          return mediaPeriodHolder.info.id.windowSequenceNumber;\n        }\n      }\n      mediaPeriodHolder = mediaPeriodHolder.getNext();\n    }\n    // If no match is found, create new sequence number.\n    long windowSequenceNumber = nextWindowSequenceNumber++;\n    if (playing == null) {\n      // If the queue is empty, save it as old front uid to allow later reuse.\n      oldFrontPeriodUid = periodUid;\n      oldFrontPeriodWindowSequenceNumber = windowSequenceNumber;\n    }\n    return windowSequenceNumber;\n  }\n\n  /**\n   * Returns whether a period described by {@code oldInfo} can be kept for playing the media period\n   * described by {@code newInfo}.\n   */\n  private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {\n    return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);\n  }\n\n  /**\n   * Returns whether a duration change of a period is compatible with keeping the following periods.\n   */\n  private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {\n    return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;\n  }\n\n  /**\n   * Updates the queue for any playback mode change, and returns whether the change was fully\n   * handled. If not, it is necessary to seek to the current playback position.\n   */\n  private boolean updateForPlaybackModeChange() {\n    // Find the last existing period holder that matches the new period order.\n    MediaPeriodHolder lastValidPeriodHolder = playing;\n    if (lastValidPeriodHolder == null) {\n      return true;\n    }\n    int currentPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.uid);\n    while (true) {\n      int nextPeriodIndex =\n          timeline.getNextPeriodIndex(\n              currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled);\n      while (lastValidPeriodHolder.getNext() != null\n          && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {\n        lastValidPeriodHolder = lastValidPeriodHolder.getNext();\n      }\n\n      MediaPeriodHolder nextMediaPeriodHolder = lastValidPeriodHolder.getNext();\n      if (nextPeriodIndex == C.INDEX_UNSET || nextMediaPeriodHolder == null) {\n        break;\n      }\n      int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(nextMediaPeriodHolder.uid);\n      if (nextPeriodHolderPeriodIndex != nextPeriodIndex) {\n        break;\n      }\n      lastValidPeriodHolder = nextMediaPeriodHolder;\n      currentPeriodIndex = nextPeriodIndex;\n    }\n\n    // Release any period holders that don't match the new period order.\n    boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder);\n\n    // Update the period info for the last holder, as it may now be the last period in the timeline.\n    lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info);\n\n    // If renderers may have read from a period that's been removed, it is necessary to restart.\n    return !readingPeriodRemoved;\n  }\n\n  /**\n   * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.\n   */\n  private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {\n    return getMediaPeriodInfo(\n        playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs);\n  }\n\n  /**\n   * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s\n   * media period.\n   *\n   * @param mediaPeriodHolder The media period holder.\n   * @param rendererPositionUs The current renderer position in microseconds.\n   * @return The following media period's info, or {@code null} if it is not yet possible to get the\n   *     next media period info.\n   */\n  private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo(\n      MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) {\n    // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod\n    // but if the timeline is not ready to provide the next period it can't return a non-null value\n    // until the timeline is updated. Store whether the next timeline period is ready when the\n    // timeline is updated, to avoid repeatedly checking the same timeline.\n    MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;\n    // The expected delay until playback transitions to the new period is equal the duration of\n    // media that's currently buffered (assuming no interruptions). This is used to project forward\n    // the start position for transitions to new windows.\n    long bufferedDurationUs =\n        mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;\n    if (mediaPeriodInfo.isLastInTimelinePeriod) {\n      int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid);\n      int nextPeriodIndex =\n          timeline.getNextPeriodIndex(\n              currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled);\n      if (nextPeriodIndex == C.INDEX_UNSET) {\n        // We can't create a next period yet.\n        return null;\n      }\n\n      long startPositionUs;\n      long contentPositionUs;\n      int nextWindowIndex =\n          timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;\n      Object nextPeriodUid = period.uid;\n      long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;\n      if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {\n        // We're starting to buffer a new window. When playback transitions to this window we'll\n        // want it to be from its default start position, so project the default start position\n        // forward by the duration of the buffer, and start buffering from this point.\n        contentPositionUs = C.TIME_UNSET;\n        Pair<Object, Long> defaultPosition =\n            timeline.getPeriodPosition(\n                window,\n                period,\n                nextWindowIndex,\n                /* windowPositionUs= */ C.TIME_UNSET,\n                /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));\n        if (defaultPosition == null) {\n          return null;\n        }\n        nextPeriodUid = defaultPosition.first;\n        startPositionUs = defaultPosition.second;\n        MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();\n        if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {\n          windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;\n        } else {\n          windowSequenceNumber = nextWindowSequenceNumber++;\n        }\n      } else {\n        // We're starting to buffer a new period within the same window.\n        startPositionUs = 0;\n        contentPositionUs = 0;\n      }\n      MediaPeriodId periodId =\n          resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);\n      return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);\n    }\n\n    MediaPeriodId currentPeriodId = mediaPeriodInfo.id;\n    timeline.getPeriodByUid(currentPeriodId.periodUid, period);\n    if (currentPeriodId.isAd()) {\n      int adGroupIndex = currentPeriodId.adGroupIndex;\n      int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex);\n      if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {\n        return null;\n      }\n      int nextAdIndexInAdGroup =\n          period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup);\n      if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {\n        // Play the next ad in the ad group if it's available.\n        return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup)\n            ? null\n            : getMediaPeriodInfoForAd(\n                currentPeriodId.periodUid,\n                adGroupIndex,\n                nextAdIndexInAdGroup,\n                mediaPeriodInfo.contentPositionUs,\n                currentPeriodId.windowSequenceNumber);\n      } else {\n        // Play content from the ad group position.\n        long startPositionUs = mediaPeriodInfo.contentPositionUs;\n        if (startPositionUs == C.TIME_UNSET) {\n          // If we're transitioning from an ad group to content starting from its default position,\n          // project the start position forward as if this were a transition to a new window.\n          Pair<Object, Long> defaultPosition =\n              timeline.getPeriodPosition(\n                  window,\n                  period,\n                  period.windowIndex,\n                  /* windowPositionUs= */ C.TIME_UNSET,\n                  /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));\n          if (defaultPosition == null) {\n            return null;\n          }\n          startPositionUs = defaultPosition.second;\n        }\n        return getMediaPeriodInfoForContent(\n            currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber);\n      }\n    } else {\n      // Play the next ad group if it's available.\n      int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);\n      if (nextAdGroupIndex == C.INDEX_UNSET) {\n        // The next ad group can't be played. Play content from the previous end position instead.\n        return getMediaPeriodInfoForContent(\n            currentPeriodId.periodUid,\n            /* startPositionUs= */ mediaPeriodInfo.durationUs,\n            currentPeriodId.windowSequenceNumber);\n      }\n      int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);\n      return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup)\n          ? null\n          : getMediaPeriodInfoForAd(\n              currentPeriodId.periodUid,\n              nextAdGroupIndex,\n              adIndexInAdGroup,\n              /* contentPositionUs= */ mediaPeriodInfo.durationUs,\n              currentPeriodId.windowSequenceNumber);\n    }\n  }\n\n  private MediaPeriodInfo getMediaPeriodInfo(\n      MediaPeriodId id, long contentPositionUs, long startPositionUs) {\n    timeline.getPeriodByUid(id.periodUid, period);\n    if (id.isAd()) {\n      if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {\n        return null;\n      }\n      return getMediaPeriodInfoForAd(\n          id.periodUid,\n          id.adGroupIndex,\n          id.adIndexInAdGroup,\n          contentPositionUs,\n          id.windowSequenceNumber);\n    } else {\n      return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber);\n    }\n  }\n\n  private MediaPeriodInfo getMediaPeriodInfoForAd(\n      Object periodUid,\n      int adGroupIndex,\n      int adIndexInAdGroup,\n      long contentPositionUs,\n      long windowSequenceNumber) {\n    MediaPeriodId id =\n        new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);\n    long durationUs =\n        timeline\n            .getPeriodByUid(id.periodUid, period)\n            .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);\n    long startPositionUs =\n        adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex)\n            ? period.getAdResumePositionUs()\n            : 0;\n    return new MediaPeriodInfo(\n        id,\n        startPositionUs,\n        contentPositionUs,\n        /* endPositionUs= */ C.TIME_UNSET,\n        durationUs,\n        /* isLastInTimelinePeriod= */ false,\n        /* isFinal= */ false);\n  }\n\n  private MediaPeriodInfo getMediaPeriodInfoForContent(\n      Object periodUid, long startPositionUs, long windowSequenceNumber) {\n    int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);\n    MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);\n    boolean isLastInPeriod = isLastInPeriod(id);\n    boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);\n    long endPositionUs =\n        nextAdGroupIndex != C.INDEX_UNSET\n            ? period.getAdGroupTimeUs(nextAdGroupIndex)\n            : C.TIME_UNSET;\n    long durationUs =\n        endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE\n            ? period.durationUs\n            : endPositionUs;\n    return new MediaPeriodInfo(\n        id,\n        startPositionUs,\n        /* contentPositionUs= */ C.TIME_UNSET,\n        endPositionUs,\n        durationUs,\n        isLastInPeriod,\n        isLastInTimeline);\n  }\n\n  private boolean isLastInPeriod(MediaPeriodId id) {\n    return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;\n  }\n\n  private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {\n    int periodIndex = timeline.getIndexOfPeriod(id.periodUid);\n    int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex;\n    return !timeline.getWindow(windowIndex, window).isDynamic\n        && timeline.isLastPeriod(periodIndex, period, window, repeatMode, shuffleModeEnabled)\n        && isLastMediaPeriodInPeriod;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport java.io.IOException;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not\n * consume data from its {@link SampleStream}.\n */\npublic abstract class NoSampleRenderer implements Renderer, RendererCapabilities {\n\n  @MonotonicNonNull private RendererConfiguration configuration;\n  private int index;\n  private int state;\n  @Nullable private SampleStream stream;\n  private boolean streamIsFinal;\n\n  @Override\n  public final int getTrackType() {\n    return C.TRACK_TYPE_NONE;\n  }\n\n  @Override\n  public final RendererCapabilities getCapabilities() {\n    return this;\n  }\n\n  @Override\n  public final void setIndex(int index) {\n    this.index = index;\n  }\n\n  @Override\n  @Nullable\n  public MediaClock getMediaClock() {\n    return null;\n  }\n\n  @Override\n  public final int getState() {\n    return state;\n  }\n\n  /**\n   * Replaces the {@link SampleStream} that will be associated with this renderer.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_DISABLED}.\n   *\n   * @param configuration The renderer configuration.\n   * @param formats The enabled formats. Should be empty.\n   * @param stream The {@link SampleStream} from which the renderer should consume.\n   * @param positionUs The player's current position.\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @param offsetUs The offset that should be subtracted from {@code positionUs}\n   *     to get the playback position with respect to the media.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  @Override\n  public final void enable(RendererConfiguration configuration, Format[] formats,\n      SampleStream stream, long positionUs, boolean joining, long offsetUs)\n      throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_DISABLED);\n    this.configuration = configuration;\n    state = STATE_ENABLED;\n    onEnabled(joining);\n    replaceStream(formats, stream, offsetUs);\n    onPositionReset(positionUs, joining);\n  }\n\n  @Override\n  public final void start() throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_ENABLED);\n    state = STATE_STARTED;\n    onStarted();\n  }\n\n  /**\n   * Replaces the {@link SampleStream} that will be associated with this renderer.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @param formats The enabled formats. Should be empty.\n   * @param stream The {@link SampleStream} to be associated with this renderer.\n   * @param offsetUs The offset that should be subtracted from {@code positionUs} in\n   *     {@link #render(long, long)} to get the playback position with respect to the media.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  @Override\n  public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)\n      throws ExoPlaybackException {\n    Assertions.checkState(!streamIsFinal);\n    this.stream = stream;\n    onRendererOffsetChanged(offsetUs);\n  }\n\n  @Override\n  @Nullable\n  public final SampleStream getStream() {\n    return stream;\n  }\n\n  @Override\n  public final boolean hasReadStreamToEnd() {\n    return true;\n  }\n\n  @Override\n  public long getReadingPositionUs() {\n    return C.TIME_END_OF_SOURCE;\n  }\n\n  @Override\n  public final void setCurrentStreamFinal() {\n    streamIsFinal = true;\n  }\n\n  @Override\n  public final boolean isCurrentStreamFinal() {\n    return streamIsFinal;\n  }\n\n  @Override\n  public final void maybeThrowStreamError() throws IOException {\n  }\n\n  @Override\n  public final void resetPosition(long positionUs) throws ExoPlaybackException {\n    streamIsFinal = false;\n    onPositionReset(positionUs, false);\n  }\n\n  @Override\n  public final void stop() throws ExoPlaybackException {\n    Assertions.checkState(state == STATE_STARTED);\n    state = STATE_ENABLED;\n    onStopped();\n  }\n\n  @Override\n  public final void disable() {\n    Assertions.checkState(state == STATE_ENABLED);\n    state = STATE_DISABLED;\n    stream = null;\n    streamIsFinal = false;\n    onDisabled();\n  }\n\n  @Override\n  public final void reset() {\n    Assertions.checkState(state == STATE_DISABLED);\n    onReset();\n  }\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return true;\n  }\n\n  // RendererCapabilities implementation.\n\n  @Override\n  @Capabilities\n  public int supportsFormat(Format format) throws ExoPlaybackException {\n    return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n  }\n\n  @Override\n  @AdaptiveSupport\n  public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n    return ADAPTIVE_NOT_SUPPORTED;\n  }\n\n  // PlayerMessage.Target implementation.\n\n  @Override\n  public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  // Methods to be overridden by subclasses.\n\n  /**\n   * Called when the renderer is enabled.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer's offset has been changed.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param offsetUs The offset that should be subtracted from {@code positionUs} in\n   *     {@link #render(long, long)} to get the playback position with respect to the media.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onRendererOffsetChanged(long offsetUs) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the position is reset. This occurs when the renderer is enabled after\n   * {@link #onRendererOffsetChanged(long)} has been called, and also when a position\n   * discontinuity is encountered.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param positionUs The new playback position in microseconds.\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is started.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onStarted() throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is stopped.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  protected void onStopped() throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is disabled.\n   * <p>\n   * The default implementation is a no-op.\n   */\n  protected void onDisabled() {\n    // Do nothing.\n  }\n\n  /**\n   * Called when the renderer is reset.\n   *\n   * <p>The default implementation is a no-op.\n   */\n  protected void onReset() {\n    // Do nothing.\n  }\n\n  // Methods to be called by subclasses.\n\n  /**\n   * Returns the configuration set when the renderer was most recently enabled, or {@code null} if\n   * the renderer has never been enabled.\n   */\n  @Nullable\n  protected final RendererConfiguration getConfiguration() {\n    return configuration;\n  }\n\n  /**\n   * Returns the index of the renderer within the player.\n   */\n  protected final int getIndex() {\n    return index;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/ParserException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport java.io.IOException;\n\n/**\n * Thrown when an error occurs parsing media data and metadata.\n */\npublic class ParserException extends IOException {\n\n  public ParserException() {\n    super();\n  }\n\n  /**\n   * @param message The detail message for the exception.\n   */\n  public ParserException(String message) {\n    super(message);\n  }\n\n  /**\n   * @param cause The cause for the exception.\n   */\n  public ParserException(Throwable cause) {\n    super(cause);\n  }\n\n  /**\n   * @param message The detail message for the exception.\n   * @param cause The cause for the exception.\n   */\n  public ParserException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\n\n/**\n * Information about an ongoing playback.\n */\n/* package */ final class PlaybackInfo {\n\n  /**\n   * Dummy media period id used while the timeline is empty and no period id is specified. This id\n   * is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}.\n   */\n  private static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID =\n      new MediaPeriodId(/* periodUid= */ new Object());\n\n  /** The current {@link Timeline}. */\n  public final Timeline timeline;\n  /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */\n  public final MediaPeriodId periodId;\n  /**\n   * The start position at which playback started in {@link #periodId} relative to the start of the\n   * associated period in the {@link #timeline}, in microseconds. Note that this value changes for\n   * each position discontinuity.\n   */\n  public final long startPositionUs;\n  /**\n   * If {@link #periodId} refers to an ad, the position of the suspended content relative to the\n   * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET}\n   * if {@link #periodId} does not refer to an ad or if the suspended content should be played from\n   * its default position.\n   */\n  public final long contentPositionUs;\n  /** The current playback state. One of the {@link Player}.STATE_ constants. */\n  @Player.State public final int playbackState;\n  /** The current playback error, or null if this is not an error state. */\n  @Nullable public final ExoPlaybackException playbackError;\n  /** Whether the player is currently loading. */\n  public final boolean isLoading;\n  /** The currently available track groups. */\n  public final TrackGroupArray trackGroups;\n  /** The result of the current track selection. */\n  public final TrackSelectorResult trackSelectorResult;\n  /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */\n  public final MediaPeriodId loadingMediaPeriodId;\n\n  /**\n   * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start\n   * of the associated period in the {@link #timeline}, in microseconds.\n   */\n  public volatile long bufferedPositionUs;\n  /**\n   * Total duration of buffered media from {@link #positionUs} to {@link #bufferedPositionUs}\n   * including all ads.\n   */\n  public volatile long totalBufferedDurationUs;\n  /**\n   * Current playback position in {@link #periodId} relative to the start of the associated period\n   * in the {@link #timeline}, in microseconds.\n   */\n  public volatile long positionUs;\n\n  /**\n   * Creates empty dummy playback info which can be used for masking as long as no real playback\n   * info is available.\n   *\n   * @param startPositionUs The start position at which playback should start, in microseconds.\n   * @param emptyTrackSelectorResult An empty track selector result with null entries for each\n   *     renderer.\n   * @return A dummy playback info.\n   */\n  public static PlaybackInfo createDummy(\n      long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) {\n    return new PlaybackInfo(\n        Timeline.EMPTY,\n        DUMMY_MEDIA_PERIOD_ID,\n        startPositionUs,\n        /* contentPositionUs= */ C.TIME_UNSET,\n        Player.STATE_IDLE,\n        /* playbackError= */ null,\n        /* isLoading= */ false,\n        TrackGroupArray.EMPTY,\n        emptyTrackSelectorResult,\n        DUMMY_MEDIA_PERIOD_ID,\n        startPositionUs,\n        /* totalBufferedDurationUs= */ 0,\n        startPositionUs);\n  }\n\n  /**\n   * Create playback info.\n   *\n   * @param timeline See {@link #timeline}.\n   * @param periodId See {@link #periodId}.\n   * @param startPositionUs See {@link #startPositionUs}.\n   * @param contentPositionUs See {@link #contentPositionUs}.\n   * @param playbackState See {@link #playbackState}.\n   * @param isLoading See {@link #isLoading}.\n   * @param trackGroups See {@link #trackGroups}.\n   * @param trackSelectorResult See {@link #trackSelectorResult}.\n   * @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}.\n   * @param bufferedPositionUs See {@link #bufferedPositionUs}.\n   * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.\n   * @param positionUs See {@link #positionUs}.\n   */\n  public PlaybackInfo(\n      Timeline timeline,\n      MediaPeriodId periodId,\n      long startPositionUs,\n      long contentPositionUs,\n      @Player.State int playbackState,\n      @Nullable ExoPlaybackException playbackError,\n      boolean isLoading,\n      TrackGroupArray trackGroups,\n      TrackSelectorResult trackSelectorResult,\n      MediaPeriodId loadingMediaPeriodId,\n      long bufferedPositionUs,\n      long totalBufferedDurationUs,\n      long positionUs) {\n    this.timeline = timeline;\n    this.periodId = periodId;\n    this.startPositionUs = startPositionUs;\n    this.contentPositionUs = contentPositionUs;\n    this.playbackState = playbackState;\n    this.playbackError = playbackError;\n    this.isLoading = isLoading;\n    this.trackGroups = trackGroups;\n    this.trackSelectorResult = trackSelectorResult;\n    this.loadingMediaPeriodId = loadingMediaPeriodId;\n    this.bufferedPositionUs = bufferedPositionUs;\n    this.totalBufferedDurationUs = totalBufferedDurationUs;\n    this.positionUs = positionUs;\n  }\n\n  /**\n   * Returns dummy media period id for the first-to-be-played period of the current timeline.\n   *\n   * @param shuffleModeEnabled Whether shuffle mode is enabled.\n   * @param window A writable {@link Timeline.Window}.\n   * @param period A writable {@link Timeline.Period}.\n   * @return A dummy media period id for the first-to-be-played period of the current timeline.\n   */\n  public MediaPeriodId getDummyFirstMediaPeriodId(\n      boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) {\n    if (timeline.isEmpty()) {\n      return DUMMY_MEDIA_PERIOD_ID;\n    }\n    int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);\n    int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;\n    int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid);\n    long windowSequenceNumber = C.INDEX_UNSET;\n    if (currentPeriodIndex != C.INDEX_UNSET) {\n      int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex;\n      if (firstWindowIndex == currentWindowIndex) {\n        // Keep window sequence number if the new position is still in the same window.\n        windowSequenceNumber = periodId.windowSequenceNumber;\n      }\n    }\n    return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber);\n  }\n\n  /**\n   * Copies playback info with new playing position.\n   *\n   * @param periodId New playing media period. See {@link #periodId}.\n   * @param positionUs New position. See {@link #positionUs}.\n   * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored\n   *     if {@code periodId.isAd()} is true.\n   * @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}.\n   * @return Copied playback info with new playing position.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithNewPosition(\n      MediaPeriodId periodId,\n      long positionUs,\n      long contentPositionUs,\n      long totalBufferedDurationUs) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        positionUs,\n        periodId.isAd() ? contentPositionUs : C.TIME_UNSET,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with the new timeline.\n   *\n   * @param timeline New timeline. See {@link #timeline}.\n   * @return Copied playback info with the new timeline.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithTimeline(Timeline timeline) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with new playback state.\n   *\n   * @param playbackState New playback state. See {@link #playbackState}.\n   * @return Copied playback info with new playback state.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithPlaybackState(int playbackState) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with a playback error.\n   *\n   * @param playbackError The error. See {@link #playbackError}.\n   * @return Copied playback info with the playback error.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithPlaybackError(@Nullable ExoPlaybackException playbackError) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with new loading state.\n   *\n   * @param isLoading New loading state. See {@link #isLoading}.\n   * @return Copied playback info with new loading state.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithIsLoading(boolean isLoading) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with new track information.\n   *\n   * @param trackGroups New track groups. See {@link #trackGroups}.\n   * @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}.\n   * @return Copied playback info with new track information.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithTrackInfo(\n      TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with new loading media period.\n   *\n   * @param loadingMediaPeriodId New loading media period id. See {@link #loadingMediaPeriodId}.\n   * @return Copied playback info with new loading media period.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) {\n    return new PlaybackInfo(\n        timeline,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        playbackError,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * The parameters that apply to playback.\n */\npublic final class PlaybackParameters {\n\n  /**\n   * The default playback parameters: real-time playback with no pitch modification or silence\n   * skipping.\n   */\n  public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f);\n\n  /** The factor by which playback will be sped up. */\n  public final float speed;\n\n  /** The factor by which the audio pitch will be scaled. */\n  public final float pitch;\n\n  /** Whether to skip silence in the input. */\n  public final boolean skipSilence;\n\n  private final int scaledUsPerMs;\n\n  /**\n   * Creates new playback parameters that set the playback speed.\n   *\n   * @param speed The factor by which playback will be sped up. Must be greater than zero.\n   */\n  public PlaybackParameters(float speed) {\n    this(speed, /* pitch= */ 1f, /* skipSilence= */ false);\n  }\n\n  /**\n   * Creates new playback parameters that set the playback speed and audio pitch scaling factor.\n   *\n   * @param speed The factor by which playback will be sped up. Must be greater than zero.\n   * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero.\n   */\n  public PlaybackParameters(float speed, float pitch) {\n    this(speed, pitch, /* skipSilence= */ false);\n  }\n\n  /**\n   * Creates new playback parameters that set the playback speed, audio pitch scaling factor and\n   * whether to skip silence in the audio stream.\n   *\n   * @param speed The factor by which playback will be sped up. Must be greater than zero.\n   * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero.\n   * @param skipSilence Whether to skip silences in the audio stream.\n   */\n  public PlaybackParameters(float speed, float pitch, boolean skipSilence) {\n    Assertions.checkArgument(speed > 0);\n    Assertions.checkArgument(pitch > 0);\n    this.speed = speed;\n    this.pitch = pitch;\n    this.skipSilence = skipSilence;\n    scaledUsPerMs = Math.round(speed * 1000f);\n  }\n\n  /**\n   * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of\n   * wallclock time.\n   *\n   * @param timeMs The time to scale, in milliseconds.\n   * @return The scaled time, in microseconds.\n   */\n  public long getMediaTimeUsForPlayoutTimeMs(long timeMs) {\n    return timeMs * scaledUsPerMs;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    PlaybackParameters other = (PlaybackParameters) obj;\n    return this.speed == other.speed\n        && this.pitch == other.pitch\n        && this.skipSilence == other.skipSilence;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + Float.floatToRawIntBits(speed);\n    result = 31 * result + Float.floatToRawIntBits(pitch);\n    result = 31 * result + (skipSilence ? 1 : 0);\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\n/** Called to prepare a playback. */\npublic interface PlaybackPreparer {\n\n  /** Called to prepare a playback. */\n  void preparePlayback();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/Player.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Looper;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C.VideoScalingMode;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.audio.AudioListener;\nimport com.google.android.exoplayer2.audio.AuxEffectInfo;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;\nimport com.google.android.exoplayer2.video.VideoFrameMetadataListener;\nimport com.google.android.exoplayer2.video.VideoListener;\nimport com.google.android.exoplayer2.video.spherical.CameraMotionListener;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * A media player interface defining traditional high-level functionality, such as the ability to\n * play, pause, seek and query properties of the currently playing media.\n * <p>\n * Some important properties of media players that implement this interface are:\n * <ul>\n *     <li>They can provide a {@link Timeline} representing the structure of the media being played,\n *     which can be obtained by calling {@link #getCurrentTimeline()}.</li>\n *     <li>They can provide a {@link TrackGroupArray} defining the currently available tracks,\n *     which can be obtained by calling {@link #getCurrentTrackGroups()}.</li>\n *     <li>They contain a number of renderers, each of which is able to render tracks of a single\n *     type (e.g. audio, video or text). The number of renderers and their respective track types\n *     can be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.\n *     </li>\n *     <li>They can provide a {@link TrackSelectionArray} defining which of the currently available\n *     tracks are selected to be rendered by each renderer. This can be obtained by calling\n *     {@link #getCurrentTrackSelections()}}.</li>\n * </ul>\n */\npublic interface Player {\n\n  /** The audio component of a {@link Player}. */\n  interface AudioComponent {\n\n    /**\n     * Adds a listener to receive audio events.\n     *\n     * @param listener The listener to register.\n     */\n    void addAudioListener(AudioListener listener);\n\n    /**\n     * Removes a listener of audio events.\n     *\n     * @param listener The listener to unregister.\n     */\n    void removeAudioListener(AudioListener listener);\n\n    /**\n     * Sets the attributes for audio playback, used by the underlying audio track. If not set, the\n     * default audio attributes will be used. They are suitable for general media playback.\n     *\n     * <p>Setting the audio attributes during playback may introduce a short gap in audio output as\n     * the audio track is recreated. A new audio session id will also be generated.\n     *\n     * <p>If tunneling is enabled by the track selector, the specified audio attributes will be\n     * ignored, but they will take effect if audio is later played without tunneling.\n     *\n     * <p>If the device is running a build before platform API version 21, audio attributes cannot\n     * be set directly on the underlying audio track. In this case, the usage will be mapped onto an\n     * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.\n     *\n     * @param audioAttributes The attributes to use for audio playback.\n     * @deprecated Use {@link AudioComponent#setAudioAttributes(AudioAttributes, boolean)}.\n     */\n    @Deprecated\n    void setAudioAttributes(AudioAttributes audioAttributes);\n\n    /**\n     * Sets the attributes for audio playback, used by the underlying audio track. If not set, the\n     * default audio attributes will be used. They are suitable for general media playback.\n     *\n     * <p>Setting the audio attributes during playback may introduce a short gap in audio output as\n     * the audio track is recreated. A new audio session id will also be generated.\n     *\n     * <p>If tunneling is enabled by the track selector, the specified audio attributes will be\n     * ignored, but they will take effect if audio is later played without tunneling.\n     *\n     * <p>If the device is running a build before platform API version 21, audio attributes cannot\n     * be set directly on the underlying audio track. In this case, the usage will be mapped onto an\n     * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.\n     *\n     * <p>If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link\n     * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link\n     * IllegalArgumentException}.\n     *\n     * @param audioAttributes The attributes to use for audio playback.\n     * @param handleAudioFocus True if the player should handle audio focus, false otherwise.\n     */\n    void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus);\n\n    /** Returns the attributes for audio playback. */\n    AudioAttributes getAudioAttributes();\n\n    /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */\n    int getAudioSessionId();\n\n    /** Sets information on an auxiliary audio effect to attach to the underlying audio track. */\n    void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);\n\n    /** Detaches any previously attached auxiliary audio effect from the underlying audio track. */\n    void clearAuxEffectInfo();\n\n    /**\n     * Sets the audio volume, with 0 being silence and 1 being unity gain.\n     *\n     * @param audioVolume The audio volume.\n     */\n    void setVolume(float audioVolume);\n\n    /** Returns the audio volume, with 0 being silence and 1 being unity gain. */\n    float getVolume();\n  }\n\n  /** The video component of a {@link Player}. */\n  interface VideoComponent {\n\n    /**\n     * Sets the {@link VideoScalingMode}.\n     *\n     * @param videoScalingMode The {@link VideoScalingMode}.\n     */\n    void setVideoScalingMode(@VideoScalingMode int videoScalingMode);\n\n    /** Returns the {@link VideoScalingMode}. */\n    @VideoScalingMode\n    int getVideoScalingMode();\n\n    /**\n     * Adds a listener to receive video events.\n     *\n     * @param listener The listener to register.\n     */\n    void addVideoListener(VideoListener listener);\n\n    /**\n     * Removes a listener of video events.\n     *\n     * @param listener The listener to unregister.\n     */\n    void removeVideoListener(VideoListener listener);\n\n    /**\n     * Sets a listener to receive video frame metadata events.\n     *\n     * <p>This method is intended to be called by the same component that sets the {@link Surface}\n     * onto which video will be rendered. If using ExoPlayer's standard UI components, this method\n     * should not be called directly from application code.\n     *\n     * @param listener The listener.\n     */\n    void setVideoFrameMetadataListener(VideoFrameMetadataListener listener);\n\n    /**\n     * Clears the listener which receives video frame metadata events if it matches the one passed.\n     * Else does nothing.\n     *\n     * @param listener The listener to clear.\n     */\n    void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener);\n\n    /**\n     * Sets a listener of camera motion events.\n     *\n     * @param listener The listener.\n     */\n    void setCameraMotionListener(CameraMotionListener listener);\n\n    /**\n     * Clears the listener which receives camera motion events if it matches the one passed. Else\n     * does nothing.\n     *\n     * @param listener The listener to clear.\n     */\n    void clearCameraMotionListener(CameraMotionListener listener);\n\n    /**\n     * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}\n     * currently set on the player.\n     */\n    void clearVideoSurface();\n\n    /**\n     * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.\n     * Else does nothing.\n     *\n     * @param surface The surface to clear.\n     */\n    void clearVideoSurface(@Nullable Surface surface);\n\n    /**\n     * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for\n     * tracking the lifecycle of the surface, and must clear the surface by calling {@code\n     * setVideoSurface(null)} if the surface is destroyed.\n     *\n     * <p>If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link\n     * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link\n     * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather\n     * than this method, since passing the holder allows the player to track the lifecycle of the\n     * surface automatically.\n     *\n     * @param surface The {@link Surface}.\n     */\n    void setVideoSurface(@Nullable Surface surface);\n\n    /**\n     * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be\n     * rendered. The player will track the lifecycle of the surface automatically.\n     *\n     * @param surfaceHolder The surface holder.\n     */\n    void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);\n\n    /**\n     * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being\n     * rendered if it matches the one passed. Else does nothing.\n     *\n     * @param surfaceHolder The surface holder to clear.\n     */\n    void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);\n\n    /**\n     * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the\n     * lifecycle of the surface automatically.\n     *\n     * @param surfaceView The surface view.\n     */\n    void setVideoSurfaceView(@Nullable SurfaceView surfaceView);\n\n    /**\n     * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one\n     * passed. Else does nothing.\n     *\n     * @param surfaceView The texture view to clear.\n     */\n    void clearVideoSurfaceView(@Nullable SurfaceView surfaceView);\n\n    /**\n     * Sets the {@link TextureView} onto which video will be rendered. The player will track the\n     * lifecycle of the surface automatically.\n     *\n     * @param textureView The texture view.\n     */\n    void setVideoTextureView(@Nullable TextureView textureView);\n\n    /**\n     * Clears the {@link TextureView} onto which video is being rendered if it matches the one\n     * passed. Else does nothing.\n     *\n     * @param textureView The texture view to clear.\n     */\n    void clearVideoTextureView(@Nullable TextureView textureView);\n\n    /**\n     * Sets the video decoder output buffer renderer. This is intended for use only with extension\n     * renderers that accept {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER}. For most use\n     * cases, an output surface or view should be passed via {@link #setVideoSurface(Surface)} or\n     * {@link #setVideoSurfaceView(SurfaceView)} instead.\n     *\n     * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer, or {@code\n     *     null} to clear the output buffer renderer.\n     */\n    void setVideoDecoderOutputBufferRenderer(\n            @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer);\n\n    /** Clears the video decoder output buffer renderer. */\n    void clearVideoDecoderOutputBufferRenderer();\n\n    /**\n     * Clears the video decoder output buffer renderer if it matches the one passed. Else does\n     * nothing.\n     *\n     * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer to clear.\n     */\n    void clearVideoDecoderOutputBufferRenderer(\n            @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer);\n  }\n\n  /** The text component of a {@link Player}. */\n  interface TextComponent {\n\n    /**\n     * Registers an output to receive text events.\n     *\n     * @param listener The output to register.\n     */\n    void addTextOutput(TextOutput listener);\n\n    /**\n     * Removes a text output.\n     *\n     * @param listener The output to remove.\n     */\n    void removeTextOutput(TextOutput listener);\n  }\n\n  /** The metadata component of a {@link Player}. */\n  interface MetadataComponent {\n\n    /**\n     * Adds a {@link MetadataOutput} to receive metadata.\n     *\n     * @param output The output to register.\n     */\n    void addMetadataOutput(MetadataOutput output);\n\n    /**\n     * Removes a {@link MetadataOutput}.\n     *\n     * @param output The output to remove.\n     */\n    void removeMetadataOutput(MetadataOutput output);\n  }\n\n  /**\n   * Listener of changes in player state. All methods have no-op default implementations to allow\n   * selective overrides.\n   */\n  interface EventListener {\n\n    /**\n     * Called when the timeline has been refreshed.\n     *\n     * <p>Note that if the timeline has changed then a position discontinuity may also have\n     * occurred. For example, the current period index may have changed as a result of periods being\n     * added or removed from the timeline. This will <em>not</em> be reported via a separate call to\n     * {@link #onPositionDiscontinuity(int)}.\n     *\n     * @param timeline The latest timeline. Never null, but may be empty.\n     * @param reason The {@link TimelineChangeReason} responsible for this timeline change.\n     */\n    @SuppressWarnings(\"deprecation\")\n    default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {\n      Object manifest = null;\n      if (timeline.getWindowCount() == 1) {\n        // Legacy behavior was to report the manifest for single window timelines only.\n        Timeline.Window window = new Timeline.Window();\n        manifest = timeline.getWindow(0, window).manifest;\n      }\n      // Call deprecated version.\n      onTimelineChanged(timeline, manifest, reason);\n    }\n\n    /**\n     * Called when the timeline and/or manifest has been refreshed.\n     *\n     * <p>Note that if the timeline has changed then a position discontinuity may also have\n     * occurred. For example, the current period index may have changed as a result of periods being\n     * added or removed from the timeline. This will <em>not</em> be reported via a separate call to\n     * {@link #onPositionDiscontinuity(int)}.\n     *\n     * @param timeline The latest timeline. Never null, but may be empty.\n     * @param manifest The latest manifest. May be null.\n     * @param reason The {@link TimelineChangeReason} responsible for this timeline change.\n     * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be\n     *     accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,\n     *     window).manifest} for a given window index.\n     */\n    @Deprecated\n    default void onTimelineChanged(\n            Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {}\n\n    /**\n     * Called when the available or selected tracks change.\n     *\n     * @param trackGroups The available tracks. Never null, but may be of length zero.\n     * @param trackSelections The track selections for each renderer. Never null and always of\n     *     length {@link #getRendererCount()}, but may contain null elements.\n     */\n    default void onTracksChanged(\n            TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}\n\n    /**\n     * Called when the player starts or stops loading the source.\n     *\n     * @param isLoading Whether the source is currently being loaded.\n     */\n    default void onLoadingChanged(boolean isLoading) {}\n\n    /**\n     * Called when the value returned from either {@link #getPlayWhenReady()} or {@link\n     * #getPlaybackState()} changes.\n     *\n     * @param playWhenReady Whether playback will proceed when ready.\n     * @param playbackState The new {@link State playback state}.\n     */\n    default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {}\n\n    /**\n     * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes.\n     *\n     * @param playbackSuppressionReason The current {@link PlaybackSuppressionReason}.\n     */\n    default void onPlaybackSuppressionReasonChanged(\n            @PlaybackSuppressionReason int playbackSuppressionReason) {}\n\n    /**\n     * Called when the value of {@link #isPlaying()} changes.\n     *\n     * @param isPlaying Whether the player is playing.\n     */\n    default void onIsPlayingChanged(boolean isPlaying) {}\n\n    /**\n     * Called when the value of {@link #getRepeatMode()} changes.\n     *\n     * @param repeatMode The {@link RepeatMode} used for playback.\n     */\n    default void onRepeatModeChanged(@RepeatMode int repeatMode) {}\n\n    /**\n     * Called when the value of {@link #getShuffleModeEnabled()} changes.\n     *\n     * @param shuffleModeEnabled Whether shuffling of windows is enabled.\n     */\n    default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {}\n\n    /**\n     * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}\n     * immediately after this method is called. The player instance can still be used, and {@link\n     * #release()} must still be called on the player should it no longer be required.\n     *\n     * @param error The error.\n     */\n    default void onPlayerError(ExoPlaybackException error) {}\n\n    /**\n     * Called when a position discontinuity occurs without a change to the timeline. A position\n     * discontinuity occurs when the current window or period index changes (as a result of playback\n     * transitioning from one period in the timeline to the next), or when the playback position\n     * jumps within the period currently being played (as a result of a seek being performed, or\n     * when the source introduces a discontinuity internally).\n     *\n     * <p>When a position discontinuity occurs as a result of a change to the timeline this method\n     * is <em>not</em> called. {@link #onTimelineChanged(Timeline, int)} is called in this case.\n     *\n     * @param reason The {@link DiscontinuityReason} responsible for the discontinuity.\n     */\n    default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}\n\n    /**\n     * Called when the current playback parameters change. The playback parameters may change due to\n     * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change\n     * them (for example, if audio playback switches to passthrough mode, where speed adjustment is\n     * no longer possible).\n     *\n     * @param playbackParameters The playback parameters.\n     */\n    default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}\n\n    /**\n     * Called when all pending seek requests have been processed by the player. This is guaranteed\n     * to happen after any necessary changes to the player state were reported to {@link\n     * #onPlayerStateChanged(boolean, int)}.\n     */\n    default void onSeekProcessed() {}\n  }\n\n  /**\n   * @deprecated Use {@link EventListener} interface directly for selective overrides as all methods\n   *     are implemented as no-op default methods.\n   */\n  @Deprecated\n  abstract class DefaultEventListener implements EventListener {\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {\n      Object manifest = null;\n      if (timeline.getWindowCount() == 1) {\n        // Legacy behavior was to report the manifest for single window timelines only.\n        Timeline.Window window = new Timeline.Window();\n        manifest = timeline.getWindow(0, window).manifest;\n      }\n      // Call deprecated version.\n      onTimelineChanged(timeline, manifest, reason);\n    }\n\n    @Override\n    @SuppressWarnings(\"deprecation\")\n    public void onTimelineChanged(\n        Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {\n      // Call deprecated version. Otherwise, do nothing.\n      onTimelineChanged(timeline, manifest);\n    }\n\n    /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */\n    @Deprecated\n    public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {\n      // Do nothing.\n    }\n  }\n\n  /**\n   * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or\n   * {@link #STATE_ENDED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})\n  @interface State {}\n  /**\n   * The player does not have any media to play.\n   */\n  int STATE_IDLE = 1;\n  /**\n   * The player is not able to immediately play from its current position. This state typically\n   * occurs when more data needs to be loaded.\n   */\n  int STATE_BUFFERING = 2;\n  /**\n   * The player is able to immediately play from its current position. The player will be playing if\n   * {@link #getPlayWhenReady()} is true, and paused otherwise.\n   */\n  int STATE_READY = 3;\n  /**\n   * The player has finished playing the media.\n   */\n  int STATE_ENDED = 4;\n\n  /**\n   * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One\n   * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link\n   * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    PLAYBACK_SUPPRESSION_REASON_NONE,\n    PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS\n  })\n  @interface PlaybackSuppressionReason {}\n  /** Playback is not suppressed. */\n  int PLAYBACK_SUPPRESSION_REASON_NONE = 0;\n  /** Playback is suppressed due to transient audio focus loss. */\n  int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1;\n\n  /**\n   * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link\n   * #REPEAT_MODE_ALL}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})\n  @interface RepeatMode {}\n  /**\n   * Normal playback without repetition.\n   */\n  int REPEAT_MODE_OFF = 0;\n  /**\n   * \"Repeat One\" mode to repeat the currently playing window infinitely.\n   */\n  int REPEAT_MODE_ONE = 1;\n  /**\n   * \"Repeat All\" mode to repeat the entire timeline infinitely.\n   */\n  int REPEAT_MODE_ALL = 2;\n\n  /**\n   * Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION},\n   * {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link\n   * #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    DISCONTINUITY_REASON_PERIOD_TRANSITION,\n    DISCONTINUITY_REASON_SEEK,\n    DISCONTINUITY_REASON_SEEK_ADJUSTMENT,\n    DISCONTINUITY_REASON_AD_INSERTION,\n    DISCONTINUITY_REASON_INTERNAL\n  })\n  @interface DiscontinuityReason {}\n  /**\n   * Automatic playback transition from one period in the timeline to the next. The period index may\n   * be the same as it was before the discontinuity in case the current period is repeated.\n   */\n  int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0;\n  /** Seek within the current period or to another period. */\n  int DISCONTINUITY_REASON_SEEK = 1;\n  /**\n   * Seek adjustment due to being unable to seek to the requested position or because the seek was\n   * permitted to be inexact.\n   */\n  int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2;\n  /** Discontinuity to or from an ad within one period in the timeline. */\n  int DISCONTINUITY_REASON_AD_INSERTION = 3;\n  /** Discontinuity introduced internally by the source. */\n  int DISCONTINUITY_REASON_INTERNAL = 4;\n\n  /**\n   * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link\n   * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    TIMELINE_CHANGE_REASON_PREPARED,\n    TIMELINE_CHANGE_REASON_RESET,\n    TIMELINE_CHANGE_REASON_DYNAMIC\n  })\n  @interface TimelineChangeReason {}\n  /** Timeline and manifest changed as a result of a player initialization with new media. */\n  int TIMELINE_CHANGE_REASON_PREPARED = 0;\n  /** Timeline and manifest changed as a result of a player reset. */\n  int TIMELINE_CHANGE_REASON_RESET = 1;\n  /**\n   * Timeline or manifest changed as a result of an dynamic update introduced by the played media.\n   */\n  int TIMELINE_CHANGE_REASON_DYNAMIC = 2;\n\n  /** Returns the component of this player for audio output, or null if audio is not supported. */\n  @Nullable\n  AudioComponent getAudioComponent();\n\n  /** Returns the component of this player for video output, or null if video is not supported. */\n  @Nullable\n  VideoComponent getVideoComponent();\n\n  /** Returns the component of this player for text output, or null if text is not supported. */\n  @Nullable\n  TextComponent getTextComponent();\n\n  /**\n   * Returns the component of this player for metadata output, or null if metadata is not supported.\n   */\n  @Nullable\n  MetadataComponent getMetadataComponent();\n\n  /**\n   * Returns the {@link Looper} associated with the application thread that's used to access the\n   * player and on which player events are received.\n   */\n  Looper getApplicationLooper();\n\n  /**\n   * Register a listener to receive events from the player. The listener's methods will be called on\n   * the thread that was used to construct the player. However, if the thread used to construct the\n   * player does not have a {@link Looper}, then the listener will be called on the main thread.\n   *\n   * @param listener The listener to register.\n   */\n  void addListener(EventListener listener);\n\n  /**\n   * Unregister a listener. The listener will no longer receive events from the player.\n   *\n   * @param listener The listener to unregister.\n   */\n  void removeListener(EventListener listener);\n\n  /**\n   * Returns the current {@link State playback state} of the player.\n   *\n   * @return The current {@link State playback state}.\n   */\n  @State\n  int getPlaybackState();\n\n  /**\n   * Returns the reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code\n   * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.\n   *\n   * @return The current {@link PlaybackSuppressionReason playback suppression reason}.\n   */\n  @PlaybackSuppressionReason\n  int getPlaybackSuppressionReason();\n\n  /**\n   * Returns whether the player is playing, i.e. {@link #getContentPosition()} is advancing.\n   *\n   * <p>If {@code false}, then at least one of the following is true:\n   *\n   * <ul>\n   *   <li>The {@link #getPlaybackState() playback state} is not {@link #STATE_READY ready}.\n   *   <li>There is no {@link #getPlayWhenReady() intention to play}.\n   *   <li>Playback is {@link #getPlaybackSuppressionReason() suppressed for other reasons}.\n   * </ul>\n   *\n   * @return Whether the player is playing.\n   */\n  boolean isPlaying();\n\n  /**\n   * Returns the error that caused playback to fail. This is the same error that will have been\n   * reported via {@link EventListener#onPlayerError(ExoPlaybackException)} at the time of\n   * failure. It can be queried using this method until {@code stop(true)} is called or the player\n   * is re-prepared.\n   *\n   * <p>Note that this method will always return {@code null} if {@link #getPlaybackState()} is not\n   * {@link #STATE_IDLE}.\n   *\n   * @return The error, or {@code null}.\n   */\n  @Nullable\n  ExoPlaybackException getPlaybackError();\n\n  /**\n   * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.\n   * <p>\n   * If the player is already in the ready state then this method can be used to pause and resume\n   * playback.\n   *\n   * @param playWhenReady Whether playback should proceed when ready.\n   */\n  void setPlayWhenReady(boolean playWhenReady);\n\n  /**\n   * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.\n   *\n   * @return Whether playback will proceed when ready.\n   */\n  boolean getPlayWhenReady();\n\n  /**\n   * Sets the {@link RepeatMode} to be used for playback.\n   *\n   * @param repeatMode The repeat mode.\n   */\n  void setRepeatMode(@RepeatMode int repeatMode);\n\n  /**\n   * Returns the current {@link RepeatMode} used for playback.\n   *\n   * @return The current repeat mode.\n   */\n  @RepeatMode int getRepeatMode();\n\n  /**\n   * Sets whether shuffling of windows is enabled.\n   *\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   */\n  void setShuffleModeEnabled(boolean shuffleModeEnabled);\n\n  /**\n   * Returns whether shuffling of windows is enabled.\n   */\n  boolean getShuffleModeEnabled();\n\n  /**\n   * Whether the player is currently loading the source.\n   *\n   * @return Whether the player is currently loading the source.\n   */\n  boolean isLoading();\n\n  /**\n   * Seeks to the default position associated with the current window. The position can depend on\n   * the type of media being played. For live streams it will typically be the live edge of the\n   * window. For other streams it will typically be the start of the window.\n   */\n  void seekToDefaultPosition();\n\n  /**\n   * Seeks to the default position associated with the specified window. The position can depend on\n   * the type of media being played. For live streams it will typically be the live edge of the\n   * window. For other streams it will typically be the start of the window.\n   *\n   * @param windowIndex The index of the window whose associated default position should be seeked\n   *     to.\n   */\n  void seekToDefaultPosition(int windowIndex);\n\n  /**\n   * Seeks to a position specified in milliseconds in the current window.\n   *\n   * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to\n   *     the window's default position.\n   */\n  void seekTo(long positionMs);\n\n  /**\n   * Seeks to a position specified in milliseconds in the specified window.\n   *\n   * @param windowIndex The index of the window.\n   * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to\n   *     the window's default position.\n   * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided\n   *     {@code windowIndex} is not within the bounds of the current timeline.\n   */\n  void seekTo(int windowIndex, long positionMs);\n\n  /**\n   * Returns whether a previous window exists, which may depend on the current repeat mode and\n   * whether shuffle mode is enabled.\n   */\n  boolean hasPrevious();\n\n  /**\n   * Seeks to the default position of the previous window in the timeline, which may depend on the\n   * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()}\n   * is {@code false}.\n   */\n  void previous();\n\n  /**\n   * Returns whether a next window exists, which may depend on the current repeat mode and whether\n   * shuffle mode is enabled.\n   */\n  boolean hasNext();\n\n  /**\n   * Seeks to the default position of the next window in the timeline, which may depend on the\n   * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is\n   * {@code false}.\n   */\n  void next();\n\n  /**\n   * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the\n   * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.\n   *\n   * <p>Playback parameters changes may cause the player to buffer. {@link\n   * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the\n   * currently active playback parameters change.\n   *\n   * @param playbackParameters The playback parameters, or {@code null} to use the defaults.\n   */\n  void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);\n\n  /**\n   * Returns the currently active playback parameters.\n   *\n   * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)\n   */\n  PlaybackParameters getPlaybackParameters();\n\n  /**\n   * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than\n   * this method if the intention is to pause playback.\n   *\n   * <p>Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The\n   * player instance can still be used, and {@link #release()} must still be called on the player if\n   * it's no longer required.\n   *\n   * <p>Calling this method does not reset the playback position.\n   */\n  void stop();\n\n  /**\n   * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather\n   * than this method if the intention is to pause playback.\n   *\n   * <p>Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The\n   * player instance can still be used, and {@link #release()} must still be called on the player if\n   * it's no longer required.\n   *\n   * @param reset Whether the player should be reset.\n   */\n  void stop(boolean reset);\n\n  /**\n   * Releases the player. This method must be called when the player is no longer required. The\n   * player must not be used after calling this method.\n   */\n  void release();\n\n  /**\n   * Returns the number of renderers.\n   */\n  int getRendererCount();\n\n  /**\n   * Returns the track type that the renderer at a given index handles.\n   *\n   * @see Renderer#getTrackType()\n   * @param index The index of the renderer.\n   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.\n   */\n  int getRendererType(int index);\n\n  /**\n   * Returns the available track groups.\n   */\n  TrackGroupArray getCurrentTrackGroups();\n\n  /**\n   * Returns the current track selections for each renderer.\n   */\n  TrackSelectionArray getCurrentTrackSelections();\n\n  /**\n   * Returns the current manifest. The type depends on the type of media being played. May be null.\n   */\n  @Nullable Object getCurrentManifest();\n\n  /**\n   * Returns the current {@link Timeline}. Never null, but may be empty.\n   */\n  Timeline getCurrentTimeline();\n\n  /**\n   * Returns the index of the period currently being played.\n   */\n  int getCurrentPeriodIndex();\n\n  /**\n   * Returns the index of the window currently being played.\n   */\n  int getCurrentWindowIndex();\n\n  /**\n   * Returns the index of the next timeline window to be played, which may depend on the current\n   * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window\n   * currently being played is the last window.\n   */\n  int getNextWindowIndex();\n\n  /**\n   * Returns the index of the previous timeline window to be played, which may depend on the current\n   * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window\n   * currently being played is the first window.\n   */\n  int getPreviousWindowIndex();\n\n  /**\n   * Returns the tag of the currently playing window in the timeline. May be null if no tag is set\n   * or the timeline is not yet available.\n   */\n  @Nullable Object getCurrentTag();\n\n  /**\n   * Returns the duration of the current content window or ad in milliseconds, or {@link\n   * C#TIME_UNSET} if the duration is not known.\n   */\n  long getDuration();\n\n  /** Returns the playback position in the current content window or ad, in milliseconds. */\n  long getCurrentPosition();\n\n  /**\n   * Returns an estimate of the position in the current content window or ad up to which data is\n   * buffered, in milliseconds.\n   */\n  long getBufferedPosition();\n\n  /**\n   * Returns an estimate of the percentage in the current content window or ad up to which data is\n   * buffered, or 0 if no estimate is available.\n   */\n  int getBufferedPercentage();\n\n  /**\n   * Returns an estimate of the total buffered duration from the current position, in milliseconds.\n   * This includes pre-buffered data for subsequent ads and windows.\n   */\n  long getTotalBufferedDuration();\n\n  /**\n   * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is\n   * empty.\n   *\n   * @see Timeline.Window#isDynamic\n   */\n  boolean isCurrentWindowDynamic();\n\n  /**\n   * Returns whether the current window is live, or {@code false} if the {@link Timeline} is empty.\n   *\n   * @see Timeline.Window#isLive\n   */\n  boolean isCurrentWindowLive();\n\n  /**\n   * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is\n   * empty.\n   *\n   * @see Timeline.Window#isSeekable\n   */\n  boolean isCurrentWindowSeekable();\n\n  /**\n   * Returns whether the player is currently playing an ad.\n   */\n  boolean isPlayingAd();\n\n  /**\n   * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period\n   * currently being played. Returns {@link C#INDEX_UNSET} otherwise.\n   */\n  int getCurrentAdGroupIndex();\n\n  /**\n   * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns\n   * {@link C#INDEX_UNSET} otherwise.\n   */\n  int getCurrentAdIndexInAdGroup();\n\n  /**\n   * If {@link #isPlayingAd()} returns {@code true}, returns the duration of the current content\n   * window in milliseconds, or {@link C#TIME_UNSET} if the duration is not known. If there is no ad\n   * playing, the returned duration is the same as that returned by {@link #getDuration()}.\n   */\n  long getContentDuration();\n\n  /**\n   * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be\n   * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad\n   * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}.\n   */\n  long getContentPosition();\n\n  /**\n   * If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in\n   * the current content window up to which data is buffered, in milliseconds. If there is no ad\n   * playing, the returned position is the same as that returned by {@link #getBufferedPosition()}.\n   */\n  long getContentBufferedPosition();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/PlayerMessage.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Defines a player message which can be sent with a {@link Sender} and received by a {@link\n * Target}.\n */\npublic final class PlayerMessage {\n\n  /** A target for messages. */\n  public interface Target {\n\n    /**\n     * Handles a message delivered to the target.\n     *\n     * @param messageType The message type.\n     * @param payload The message payload.\n     * @throws ExoPlaybackException If an error occurred whilst handling the message. Should only be\n     *     thrown by targets that handle messages on the playback thread.\n     */\n    void handleMessage(int messageType, @Nullable Object payload) throws ExoPlaybackException;\n  }\n\n  /** A sender for messages. */\n  public interface Sender {\n\n    /**\n     * Sends a message.\n     *\n     * @param message The message to be sent.\n     */\n    void sendMessage(PlayerMessage message);\n  }\n\n  private final Target target;\n  private final Sender sender;\n  private final Timeline timeline;\n\n  private int type;\n  @Nullable private Object payload;\n  private Handler handler;\n  private int windowIndex;\n  private long positionMs;\n  private boolean deleteAfterDelivery;\n  private boolean isSent;\n  private boolean isDelivered;\n  private boolean isProcessed;\n  private boolean isCanceled;\n\n  /**\n   * Creates a new message.\n   *\n   * @param sender The {@link Sender} used to send the message.\n   * @param target The {@link Target} the message is sent to.\n   * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If\n   *     set to {@link Timeline#EMPTY}, any position can be specified.\n   * @param defaultWindowIndex The default window index in the {@code timeline} when no other window\n   *     index is specified.\n   * @param defaultHandler The default handler to send the message on when no other handler is\n   *     specified.\n   */\n  public PlayerMessage(\n      Sender sender,\n      Target target,\n      Timeline timeline,\n      int defaultWindowIndex,\n      Handler defaultHandler) {\n    this.sender = sender;\n    this.target = target;\n    this.timeline = timeline;\n    this.handler = defaultHandler;\n    this.windowIndex = defaultWindowIndex;\n    this.positionMs = C.TIME_UNSET;\n    this.deleteAfterDelivery = true;\n  }\n\n  /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */\n  public Timeline getTimeline() {\n    return timeline;\n  }\n\n  /** Returns the target the message is sent to. */\n  public Target getTarget() {\n    return target;\n  }\n\n  /**\n   * Sets the message type forwarded to {@link Target#handleMessage(int, Object)}.\n   *\n   * @param messageType The message type.\n   * @return This message.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setType(int messageType) {\n    Assertions.checkState(!isSent);\n    this.type = messageType;\n    return this;\n  }\n\n  /** Returns the message type forwarded to {@link Target#handleMessage(int, Object)}. */\n  public int getType() {\n    return type;\n  }\n\n  /**\n   * Sets the message payload forwarded to {@link Target#handleMessage(int, Object)}.\n   *\n   * @param payload The message payload.\n   * @return This message.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setPayload(@Nullable Object payload) {\n    Assertions.checkState(!isSent);\n    this.payload = payload;\n    return this;\n  }\n\n  /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */\n  @Nullable\n  public Object getPayload() {\n    return payload;\n  }\n\n  /**\n   * Sets the handler the message is delivered on.\n   *\n   * @param handler A {@link Handler}.\n   * @return This message.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setHandler(Handler handler) {\n    Assertions.checkState(!isSent);\n    this.handler = handler;\n    return this;\n  }\n\n  /** Returns the handler the message is delivered on. */\n  public Handler getHandler() {\n    return handler;\n  }\n\n  /**\n   * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered,\n   * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately.\n   */\n  public long getPositionMs() {\n    return positionMs;\n  }\n\n  /**\n   * Sets a position in the current window at which the message will be delivered.\n   *\n   * @param positionMs The position in the current window at which the message will be sent, in\n   *     milliseconds.\n   * @return This message.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setPosition(long positionMs) {\n    Assertions.checkState(!isSent);\n    this.positionMs = positionMs;\n    return this;\n  }\n\n  /**\n   * Sets a position in a window at which the message will be delivered.\n   *\n   * @param windowIndex The index of the window at which the message will be sent.\n   * @param positionMs The position in the window with index {@code windowIndex} at which the\n   *     message will be sent, in milliseconds.\n   * @return This message.\n   * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not\n   *     empty and the provided window index is not within the bounds of the timeline.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setPosition(int windowIndex, long positionMs) {\n    Assertions.checkState(!isSent);\n    Assertions.checkArgument(positionMs != C.TIME_UNSET);\n    if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {\n      throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);\n    }\n    this.windowIndex = windowIndex;\n    this.positionMs = positionMs;\n    return this;\n  }\n\n  /** Returns window index at which the message will be delivered. */\n  public int getWindowIndex() {\n    return windowIndex;\n  }\n\n  /**\n   * Sets whether the message will be deleted after delivery. If false, the message will be resent\n   * if playback reaches the specified position again. Only allowed to be false if a position is set\n   * with {@link #setPosition(long)}.\n   *\n   * @param deleteAfterDelivery Whether the message is deleted after delivery.\n   * @return This message.\n   * @throws IllegalStateException If {@link #send()} has already been called.\n   */\n  public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) {\n    Assertions.checkState(!isSent);\n    this.deleteAfterDelivery = deleteAfterDelivery;\n    return this;\n  }\n\n  /** Returns whether the message will be deleted after delivery. */\n  public boolean getDeleteAfterDelivery() {\n    return deleteAfterDelivery;\n  }\n\n  /**\n   * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated\n   * out of the player as an error using {@link\n   * Player.EventListener#onPlayerError(ExoPlaybackException)}.\n   *\n   * @return This message.\n   * @throws IllegalStateException If this message has already been sent.\n   */\n  public PlayerMessage send() {\n    Assertions.checkState(!isSent);\n    if (positionMs == C.TIME_UNSET) {\n      Assertions.checkArgument(deleteAfterDelivery);\n    }\n    isSent = true;\n    sender.sendMessage(this);\n    return this;\n  }\n\n  /**\n   * Cancels the message delivery.\n   *\n   * @return This message.\n   * @throws IllegalStateException If this method is called before {@link #send()}.\n   */\n  public synchronized PlayerMessage cancel() {\n    Assertions.checkState(isSent);\n    isCanceled = true;\n    markAsProcessed(/* isDelivered= */ false);\n    return this;\n  }\n\n  /** Returns whether the message delivery has been canceled. */\n  public synchronized boolean isCanceled() {\n    return isCanceled;\n  }\n\n  /**\n   * Blocks until after the message has been delivered or the player is no longer able to deliver\n   * the message.\n   *\n   * <p>Note that this method can't be called if the current thread is the same thread used by the\n   * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock.\n   *\n   * @return Whether the message was delivered successfully.\n   * @throws IllegalStateException If this method is called before {@link #send()}.\n   * @throws IllegalStateException If this method is called on the same thread used by the message\n   *     handler set with {@link #setHandler(Handler)}.\n   * @throws InterruptedException If the current thread is interrupted while waiting for the message\n   *     to be delivered.\n   */\n  public synchronized boolean blockUntilDelivered() throws InterruptedException {\n    Assertions.checkState(isSent);\n    Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread());\n    while (!isProcessed) {\n      wait();\n    }\n    return isDelivered;\n  }\n\n  /**\n   * Marks the message as processed. Should only be called by a {@link Sender} and may be called\n   * multiple times.\n   *\n   * @param isDelivered Whether the message has been delivered to its target. The message is\n   *     considered as being delivered when this method has been called with {@code isDelivered} set\n   *     to true at least once.\n   */\n  public synchronized void markAsProcessed(boolean isDelivered) {\n    this.isDelivered |= isDelivered;\n    isProcessed = true;\n    notifyAll();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/Renderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Renders media read from a {@link SampleStream}.\n *\n * <p>Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is\n * transitioned through various states as the overall playback state and enabled tracks change. The\n * valid state transitions are shown below, annotated with the methods that are called during each\n * transition.\n *\n * <p align=\"center\"><img src=\"doc-files/renderer-states.svg\" alt=\"Renderer state transitions\">\n */\npublic interface Renderer extends PlayerMessage.Target {\n\n  /**\n   * The renderer states. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} or {@link\n   * #STATE_STARTED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED})\n  @interface State {}\n  /**\n   * The renderer is disabled. A renderer in this state may hold resources that it requires for\n   * rendering (e.g. media decoders), for use if it's subsequently enabled. {@link #reset()} can be\n   * called to force the renderer to release these resources.\n   */\n  int STATE_DISABLED = 0;\n  /**\n   * The renderer is enabled but not started. A renderer in this state may render media at the\n   * current position (e.g. an initial video frame), but the position will not advance. A renderer\n   * in this state will typically hold resources that it requires for rendering (e.g. media\n   * decoders).\n   */\n  int STATE_ENABLED = 1;\n  /**\n   * The renderer is started. Calls to {@link #render(long, long)} will cause media to be rendered.\n   */\n  int STATE_STARTED = 2;\n\n  /**\n   * Returns the track type that the {@link Renderer} handles. For example, a video renderer will\n   * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a\n   * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.\n   *\n   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.\n   */\n  int getTrackType();\n\n  /**\n   * Returns the capabilities of the renderer.\n   *\n   * @return The capabilities of the renderer.\n   */\n  RendererCapabilities getCapabilities();\n\n  /**\n   * Sets the index of this renderer within the player.\n   *\n   * @param index The renderer index.\n   */\n  void setIndex(int index);\n\n  /**\n   * If the renderer advances its own playback position then this method returns a corresponding\n   * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its\n   * source of time during playback. A player may have at most one renderer that returns a {@link\n   * MediaClock} from this method.\n   *\n   * @return The {@link MediaClock} tracking the playback position of the renderer, or null.\n   */\n  @Nullable\n  MediaClock getMediaClock();\n\n  /**\n   * Returns the current state of the renderer.\n   *\n   * @return The current state. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} and {@link\n   *     #STATE_STARTED}.\n   */\n  @State\n  int getState();\n\n  /**\n   * Enables the renderer to consume from the specified {@link SampleStream}.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_DISABLED}.\n   *\n   * @param configuration The renderer configuration.\n   * @param formats The enabled formats.\n   * @param stream The {@link SampleStream} from which the renderer should consume.\n   * @param positionUs The player's current position.\n   * @param joining Whether this renderer is being enabled to join an ongoing playback.\n   * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream}\n   *     before they are rendered.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,\n              long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;\n\n  /**\n   * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be\n   * rendered.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  void start() throws ExoPlaybackException;\n\n  /**\n   * Replaces the {@link SampleStream} from which samples will be consumed.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @param formats The enabled formats.\n   * @param stream The {@link SampleStream} from which the renderer should consume.\n   * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before\n   *     they are rendered.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  void replaceStream(Format[] formats, SampleStream stream, long offsetUs)\n      throws ExoPlaybackException;\n\n  /** Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. */\n  @Nullable\n  SampleStream getStream();\n\n  /**\n   * Returns whether the renderer has read the current {@link SampleStream} to the end.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   */\n  boolean hasReadStreamToEnd();\n\n  /**\n   * Returns the playback position up to which the renderer has read samples from the current {@link\n   * SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the\n   * current {@link SampleStream} to the end.\n   *\n   * <p>This method may be called when the renderer is in the following states: {@link\n   * #STATE_ENABLED}, {@link #STATE_STARTED}.\n   */\n  long getReadingPositionUs();\n\n  /**\n   * Signals to the renderer that the current {@link SampleStream} will be the final one supplied\n   * before it is next disabled or reset.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   */\n  void setCurrentStreamFinal();\n\n  /**\n   * Returns whether the current {@link SampleStream} will be the final one supplied before the\n   * renderer is next disabled or reset.\n   */\n  boolean isCurrentStreamFinal();\n\n  /**\n   * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does\n   * nothing if no such error exists.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @throws IOException An error that's preventing the renderer from making progress or buffering\n   *     more data.\n   */\n  void maybeThrowStreamError() throws IOException;\n\n  /**\n   * Signals to the renderer that a position discontinuity has occurred.\n   * <p>\n   * After a position discontinuity, the renderer's {@link SampleStream} is guaranteed to provide\n   * samples starting from a key frame.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @param positionUs The new playback position in microseconds.\n   * @throws ExoPlaybackException If an error occurs handling the reset.\n   */\n  void resetPosition(long positionUs) throws ExoPlaybackException;\n\n  /**\n   * Sets the operating rate of this renderer, where 1 is the default rate, 2 is twice the default\n   * rate, 0.5 is half the default rate and so on. The operating rate is a hint to the renderer of\n   * the speed at which playback will proceed, and may be used for resource planning.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param operatingRate The operating rate.\n   * @throws ExoPlaybackException If an error occurs handling the operating rate.\n   */\n  default void setOperatingRate(float operatingRate) throws ExoPlaybackException {}\n\n  /**\n   * Incrementally renders the {@link SampleStream}.\n   * <p>\n   * If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do\n   * work toward being ready to render the {@link SampleStream} when the renderer is started. It may\n   * also render the very start of the media, for example the first frame of a video stream. If the\n   * renderer is in the {@link #STATE_STARTED} state then calls to this method will render the\n   * {@link SampleStream} in sync with the specified media positions.\n   * <p>\n   * This method should return quickly, and should not block if the renderer is unable to make\n   * useful progress.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @param positionUs The current media time in microseconds, measured at the start of the\n   *     current iteration of the rendering loop.\n   * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;\n\n  /**\n   * Whether the renderer is able to immediately render media from the current position.\n   * <p>\n   * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the\n   * renderer has everything that it needs to continue playback. Returning false indicates that\n   * the player should pause until the renderer is ready.\n   * <p>\n   * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the\n   * renderer is ready for playback to be started. Returning false indicates that it is not.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @return Whether the renderer is ready to render media.\n   */\n  boolean isReady();\n\n  /**\n   * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to\n   * {@link Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is\n   * returned by all of its {@link Renderer}s.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.\n   *\n   * @return Whether the renderer is ready for the player to transition to the ended state.\n   */\n  boolean isEnded();\n\n  /**\n   * Stops the renderer, transitioning it to the {@link #STATE_ENABLED} state.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_STARTED}.\n   *\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  void stop() throws ExoPlaybackException;\n\n  /**\n   * Disable the renderer, transitioning it to the {@link #STATE_DISABLED} state.\n   * <p>\n   * This method may be called when the renderer is in the following states:\n   * {@link #STATE_ENABLED}.\n   */\n  void disable();\n\n  /**\n   * Forces the renderer to give up any resources (e.g. media decoders) that it may be holding. If\n   * the renderer is not holding any resources, the call is a no-op.\n   *\n   * <p>This method may be called when the renderer is in the following states: {@link\n   * #STATE_DISABLED}.\n   */\n  void reset();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.annotation.SuppressLint;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Defines the capabilities of a {@link Renderer}.\n */\npublic interface RendererCapabilities {\n\n  /**\n   * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link\n   * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link\n   * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    FORMAT_HANDLED,\n    FORMAT_EXCEEDS_CAPABILITIES,\n    FORMAT_UNSUPPORTED_DRM,\n    FORMAT_UNSUPPORTED_SUBTYPE,\n    FORMAT_UNSUPPORTED_TYPE\n  })\n  @interface FormatSupport {}\n\n  /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */\n  int FORMAT_SUPPORT_MASK = 0b111;\n  /**\n   * The {@link Renderer} is capable of rendering the format.\n   */\n  int FORMAT_HANDLED = 0b100;\n  /**\n   * The {@link Renderer} is capable of rendering formats with the same mime type, but the\n   * properties of the format exceed the renderer's capabilities. There is a chance the renderer\n   * will be able to play the format in practice because some renderers report their capabilities\n   * conservatively, but the expected outcome is that playback will fail.\n   * <p>\n   * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is\n   * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported\n   * by the underlying H264 decoder.\n   */\n  int FORMAT_EXCEEDS_CAPABILITIES = 0b011;\n  /**\n   * The {@link Renderer} is capable of rendering formats with the same mime type, but is not\n   * capable of rendering the format because the format's drm protection is not supported.\n   * <p>\n   * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is\n   * {@link MimeTypes#VIDEO_H264}, but the format indicates PlayReady drm protection where-as the\n   * renderer only supports Widevine.\n   */\n  int FORMAT_UNSUPPORTED_DRM = 0b010;\n  /**\n   * The {@link Renderer} is a general purpose renderer for formats of the same top-level type,\n   * but is not capable of rendering the format or any other format with the same mime type because\n   * the sub-type is not supported.\n   * <p>\n   * Example: The {@link Renderer} is a general purpose audio renderer and the format's\n   * mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype].\n   */\n  int FORMAT_UNSUPPORTED_SUBTYPE = 0b001;\n  /**\n   * The {@link Renderer} is not capable of rendering the format, either because it does not\n   * support the format's top-level type, or because it's a specialized renderer for a different\n   * mime type.\n   * <p>\n   * Example: The {@link Renderer} is a general purpose video renderer, but the format has an\n   * audio mime type.\n   */\n  int FORMAT_UNSUPPORTED_TYPE = 0b000;\n\n  /**\n   * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS},\n   * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED})\n  @interface AdaptiveSupport {}\n\n  /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */\n  int ADAPTIVE_SUPPORT_MASK = 0b11000;\n  /**\n   * The {@link Renderer} can seamlessly adapt between formats.\n   */\n  int ADAPTIVE_SEAMLESS = 0b10000;\n  /**\n   * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity\n   * (~50-100ms) when adaptation occurs.\n   */\n  int ADAPTIVE_NOT_SEAMLESS = 0b01000;\n  /**\n   * The {@link Renderer} does not support adaptation between formats.\n   */\n  int ADAPTIVE_NOT_SUPPORTED = 0b00000;\n\n  /**\n   * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link\n   * #TUNNELING_NOT_SUPPORTED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED})\n  @interface TunnelingSupport {}\n\n  /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */\n  int TUNNELING_SUPPORT_MASK = 0b100000;\n  /**\n   * The {@link Renderer} supports tunneled output.\n   */\n  int TUNNELING_SUPPORTED = 0b100000;\n  /**\n   * The {@link Renderer} does not support tunneled output.\n   */\n  int TUNNELING_NOT_SUPPORTED = 0b000000;\n\n  /**\n   * Combined renderer capabilities.\n   *\n   * <p>This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link\n   * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or\n   * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)}\n   * or {@link #create(int, int, int)} to create the combined capabilities.\n   *\n   * <p>Possible values:\n   *\n   * <ul>\n   *   <li>{@link FormatSupport}: The level of support for the format itself. One of {@link\n   *       #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM},\n   *       {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.\n   *   <li>{@link AdaptiveSupport}: The level of support for adapting from the format to another\n   *       format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link\n   *       #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of\n   *       support for the format itself is {@link #FORMAT_HANDLED} or {@link\n   *       #FORMAT_EXCEEDS_CAPABILITIES}.\n   *   <li>{@link TunnelingSupport}: The level of support for tunneling. One of {@link\n   *       #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of\n   *       support for the format itself is {@link #FORMAT_HANDLED} or {@link\n   *       #FORMAT_EXCEEDS_CAPABILITIES}.\n   * </ul>\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  // Intentionally empty to prevent assignment or comparison with individual flags without masking.\n  @IntDef({})\n  @interface Capabilities {}\n\n  /**\n   * Returns {@link Capabilities} for the given {@link FormatSupport}.\n   *\n   * <p>The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link\n   * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}.\n   *\n   * @param formatSupport The {@link FormatSupport}.\n   * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link\n   *     #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.\n   */\n  @Capabilities\n  static int create(@FormatSupport int formatSupport) {\n    return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED);\n  }\n\n  /**\n   * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport}\n   * and {@link TunnelingSupport}.\n   *\n   * @param formatSupport The {@link FormatSupport}.\n   * @param adaptiveSupport The {@link AdaptiveSupport}.\n   * @param tunnelingSupport The {@link TunnelingSupport}.\n   * @return The combined {@link Capabilities}.\n   */\n  // Suppression needed for IntDef casting.\n  @SuppressLint(\"WrongConstant\")\n  @Capabilities\n  static int create(\n          @FormatSupport int formatSupport,\n          @AdaptiveSupport int adaptiveSupport,\n          @TunnelingSupport int tunnelingSupport) {\n    return formatSupport | adaptiveSupport | tunnelingSupport;\n  }\n\n  /**\n   * Returns the {@link FormatSupport} from the combined {@link Capabilities}.\n   *\n   * @param supportFlags The combined {@link Capabilities}.\n   * @return The {@link FormatSupport} only.\n   */\n  // Suppression needed for IntDef casting.\n  @SuppressLint(\"WrongConstant\")\n  @FormatSupport\n  static int getFormatSupport(@Capabilities int supportFlags) {\n    return supportFlags & FORMAT_SUPPORT_MASK;\n  }\n\n  /**\n   * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}.\n   *\n   * @param supportFlags The combined {@link Capabilities}.\n   * @return The {@link AdaptiveSupport} only.\n   */\n  // Suppression needed for IntDef casting.\n  @SuppressLint(\"WrongConstant\")\n  @AdaptiveSupport\n  static int getAdaptiveSupport(@Capabilities int supportFlags) {\n    return supportFlags & ADAPTIVE_SUPPORT_MASK;\n  }\n\n  /**\n   * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}.\n   *\n   * @param supportFlags The combined {@link Capabilities}.\n   * @return The {@link TunnelingSupport} only.\n   */\n  // Suppression needed for IntDef casting.\n  @SuppressLint(\"WrongConstant\")\n  @TunnelingSupport\n  static int getTunnelingSupport(@Capabilities int supportFlags) {\n    return supportFlags & TUNNELING_SUPPORT_MASK;\n  }\n\n  /**\n   * Returns string representation of a {@link FormatSupport} flag.\n   *\n   * @param formatSupport A {@link FormatSupport} flag.\n   * @return A string representation of the flag.\n   */\n  static String getFormatSupportString(@FormatSupport int formatSupport) {\n    switch (formatSupport) {\n      case RendererCapabilities.FORMAT_HANDLED:\n        return \"YES\";\n      case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:\n        return \"NO_EXCEEDS_CAPABILITIES\";\n      case RendererCapabilities.FORMAT_UNSUPPORTED_DRM:\n        return \"NO_UNSUPPORTED_DRM\";\n      case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:\n        return \"NO_UNSUPPORTED_TYPE\";\n      case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:\n        return \"NO\";\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Returns the track type that the {@link Renderer} handles. For example, a video renderer will\n   * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a\n   * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.\n   *\n   * @see Renderer#getTrackType()\n   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.\n   */\n  int getTrackType();\n\n  /**\n   * Returns the extent to which the {@link Renderer} supports a given format.\n   *\n   * @param format The format.\n   * @return The {@link Capabilities} for this format.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  @Capabilities\n  int supportsFormat(Format format) throws ExoPlaybackException;\n\n  /**\n   * Returns the extent to which the {@link Renderer} supports adapting between supported formats\n   * that have different MIME types.\n   *\n   * @return The {@link AdaptiveSupport} for adapting between supported formats that have different\n   *     MIME types.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  @AdaptiveSupport\n  int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\n\n/**\n * The configuration of a {@link Renderer}.\n */\npublic final class RendererConfiguration {\n\n  /**\n   * The default configuration.\n   */\n  public static final RendererConfiguration DEFAULT =\n      new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);\n\n  /**\n   * The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling\n   * should not be enabled.\n   */\n  public final int tunnelingAudioSessionId;\n\n  /**\n   * @param tunnelingAudioSessionId The audio session id to use for tunneling, or\n   *     {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.\n   */\n  public RendererConfiguration(int tunnelingAudioSessionId) {\n    this.tunnelingAudioSessionId = tunnelingAudioSessionId;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    RendererConfiguration other = (RendererConfiguration) obj;\n    return tunnelingAudioSessionId == other.tunnelingAudioSessionId;\n  }\n\n  @Override\n  public int hashCode() {\n    return tunnelingAudioSessionId;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/RenderersFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\n\n/**\n * Builds {@link Renderer} instances for use by a {@link SimpleExoPlayer}.\n */\npublic interface RenderersFactory {\n\n  /**\n   * Builds the {@link Renderer} instances for a {@link SimpleExoPlayer}.\n   *\n   * @param eventHandler A handler to use when invoking event listeners and outputs.\n   * @param videoRendererEventListener An event listener for video renderers.\n   * @param audioRendererEventListener An event listener for audio renderers.\n   * @param textRendererOutput An output for text renderers.\n   * @param metadataRendererOutput An output for metadata renderers.\n   * @param drmSessionManager A drm session manager used by renderers.\n   * @return The {@link Renderer instances}.\n   */\n  Renderer[] createRenderers(\n          Handler eventHandler,\n          VideoRendererEventListener videoRendererEventListener,\n          AudioRendererEventListener audioRendererEventListener,\n          TextOutput textRendererOutput,\n          MetadataOutput metadataRendererOutput,\n          @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/SeekParameters.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Parameters that apply to seeking.\n *\n * <p>The predefined {@link #EXACT}, {@link #CLOSEST_SYNC}, {@link #PREVIOUS_SYNC} and {@link\n * #NEXT_SYNC} parameters are suitable for most use cases. Seeking to sync points is typically\n * faster but less accurate than exact seeking.\n *\n * <p>In the general case, an instance specifies a maximum tolerance before ({@link\n * #toleranceBeforeUs}) and after ({@link #toleranceAfterUs}) a requested seek position ({@code x}).\n * If one or more sync points falls within the window {@code [x - toleranceBeforeUs, x +\n * toleranceAfterUs]} then the seek will be performed to the sync point within the window that's\n * closest to {@code x}. If no sync point falls within the window then the seek will be performed to\n * {@code x - toleranceBeforeUs}. Internally the player may need to seek to an earlier sync point\n * and discard media until this position is reached.\n */\npublic final class SeekParameters {\n\n  /** Parameters for exact seeking. */\n  public static final SeekParameters EXACT = new SeekParameters(0, 0);\n  /** Parameters for seeking to the closest sync point. */\n  public static final SeekParameters CLOSEST_SYNC =\n      new SeekParameters(Long.MAX_VALUE, Long.MAX_VALUE);\n  /** Parameters for seeking to the sync point immediately before a requested seek position. */\n  public static final SeekParameters PREVIOUS_SYNC = new SeekParameters(Long.MAX_VALUE, 0);\n  /** Parameters for seeking to the sync point immediately after a requested seek position. */\n  public static final SeekParameters NEXT_SYNC = new SeekParameters(0, Long.MAX_VALUE);\n  /** Default parameters. */\n  public static final SeekParameters DEFAULT = EXACT;\n\n  /**\n   * The maximum time that the actual position seeked to may precede the requested seek position, in\n   * microseconds.\n   */\n  public final long toleranceBeforeUs;\n  /**\n   * The maximum time that the actual position seeked to may exceed the requested seek position, in\n   * microseconds.\n   */\n  public final long toleranceAfterUs;\n\n  /**\n   * @param toleranceBeforeUs The maximum time that the actual position seeked to may precede the\n   *     requested seek position, in microseconds. Must be non-negative.\n   * @param toleranceAfterUs The maximum time that the actual position seeked to may exceed the\n   *     requested seek position, in microseconds. Must be non-negative.\n   */\n  public SeekParameters(long toleranceBeforeUs, long toleranceAfterUs) {\n    Assertions.checkArgument(toleranceBeforeUs >= 0);\n    Assertions.checkArgument(toleranceAfterUs >= 0);\n    this.toleranceBeforeUs = toleranceBeforeUs;\n    this.toleranceAfterUs = toleranceAfterUs;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    SeekParameters other = (SeekParameters) obj;\n    return toleranceBeforeUs == other.toleranceBeforeUs\n        && toleranceAfterUs == other.toleranceAfterUs;\n  }\n\n  @Override\n  public int hashCode() {\n    return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.graphics.SurfaceTexture;\nimport android.media.MediaCodec;\nimport android.media.PlaybackParams;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.analytics.AnalyticsCollector;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.audio.AudioListener;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.AuxEffectInfo;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;\nimport com.google.android.exoplayer2.video.VideoFrameMetadataListener;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport com.google.android.exoplayer2.video.spherical.CameraMotionListener;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\n/**\n * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can\n * be obtained from {@link Builder}.\n */\npublic class SimpleExoPlayer extends BasePlayer\n    implements ExoPlayer,\n        Player.AudioComponent,\n        Player.VideoComponent,\n        Player.TextComponent,\n        Player.MetadataComponent {\n\n  /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */\n  @Deprecated\n  public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {}\n\n  /**\n   * A builder for {@link SimpleExoPlayer} instances.\n   *\n   * <p>See {@link #Builder(Context)} for the list of default values.\n   */\n  public static final class Builder {\n\n    private final Context context;\n    private final RenderersFactory renderersFactory;\n\n    private Clock clock;\n    private TrackSelector trackSelector;\n    private LoadControl loadControl;\n    private BandwidthMeter bandwidthMeter;\n    private AnalyticsCollector analyticsCollector;\n    private Looper looper;\n    private boolean useLazyPreparation;\n    private boolean buildCalled;\n\n    /**\n     * Creates a builder.\n     *\n     * <p>Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom\n     * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link\n     * DefaultRenderersFactory} from the APK.\n     *\n     * <p>The builder uses the following default values:\n     *\n     * <ul>\n     *   <li>{@link RenderersFactory}: {@link DefaultRenderersFactory}\n     *   <li>{@link TrackSelector}: {@link DefaultTrackSelector}\n     *   <li>{@link LoadControl}: {@link DefaultLoadControl}\n     *   <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}\n     *   <li>{@link Looper}: The {@link Looper} associated with the current thread, or the {@link\n     *       Looper} of the application's main thread if the current thread doesn't have a {@link\n     *       Looper}\n     *   <li>{@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT}\n     *   <li>{@code useLazyPreparation}: {@code true}\n     *   <li>{@link Clock}: {@link Clock#DEFAULT}\n     * </ul>\n     *\n     * @param context A {@link Context}.\n     */\n    public Builder(Context context) {\n      this(context, new DefaultRenderersFactory(context));\n    }\n\n    /**\n     * Creates a builder with a custom {@link RenderersFactory}.\n     *\n     * <p>See {@link #Builder(Context)} for a list of default values.\n     *\n     * @param context A {@link Context}.\n     * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the\n     *     player.\n     */\n    public Builder(Context context, RenderersFactory renderersFactory) {\n      this(\n          context,\n          renderersFactory,\n          new DefaultTrackSelector(context),\n          new DefaultLoadControl(),\n          DefaultBandwidthMeter.getSingletonInstance(context),\n          Util.getLooper(),\n          new AnalyticsCollector(Clock.DEFAULT),\n          /* useLazyPreparation= */ true,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * Creates a builder with the specified custom components.\n     *\n     * <p>Note that this constructor is only useful if you try to ensure that ExoPlayer's default\n     * components can be removed by ProGuard or R8. For most components except renderers, there is\n     * only a marginal benefit of doing that.\n     *\n     * @param context A {@link Context}.\n     * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the\n     *     player.\n     * @param trackSelector A {@link TrackSelector}.\n     * @param loadControl A {@link LoadControl}.\n     * @param bandwidthMeter A {@link BandwidthMeter}.\n     * @param looper A {@link Looper} that must be used for all calls to the player.\n     * @param analyticsCollector An {@link AnalyticsCollector}.\n     * @param useLazyPreparation Whether media sources should be initialized lazily.\n     * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}.\n     */\n    public Builder(\n        Context context,\n        RenderersFactory renderersFactory,\n        TrackSelector trackSelector,\n        LoadControl loadControl,\n        BandwidthMeter bandwidthMeter,\n        Looper looper,\n        AnalyticsCollector analyticsCollector,\n        boolean useLazyPreparation,\n        Clock clock) {\n      this.context = context;\n      this.renderersFactory = renderersFactory;\n      this.trackSelector = trackSelector;\n      this.loadControl = loadControl;\n      this.bandwidthMeter = bandwidthMeter;\n      this.looper = looper;\n      this.analyticsCollector = analyticsCollector;\n      this.useLazyPreparation = useLazyPreparation;\n      this.clock = clock;\n    }\n\n    /**\n     * Sets the {@link TrackSelector} that will be used by the player.\n     *\n     * @param trackSelector A {@link TrackSelector}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setTrackSelector(TrackSelector trackSelector) {\n      Assertions.checkState(!buildCalled);\n      this.trackSelector = trackSelector;\n      return this;\n    }\n\n    /**\n     * Sets the {@link LoadControl} that will be used by the player.\n     *\n     * @param loadControl A {@link LoadControl}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setLoadControl(LoadControl loadControl) {\n      Assertions.checkState(!buildCalled);\n      this.loadControl = loadControl;\n      return this;\n    }\n\n    /**\n     * Sets the {@link BandwidthMeter} that will be used by the player.\n     *\n     * @param bandwidthMeter A {@link BandwidthMeter}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) {\n      Assertions.checkState(!buildCalled);\n      this.bandwidthMeter = bandwidthMeter;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Looper} that must be used for all calls to the player and that is used to\n     * call listeners on.\n     *\n     * @param looper A {@link Looper}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setLooper(Looper looper) {\n      Assertions.checkState(!buildCalled);\n      this.looper = looper;\n      return this;\n    }\n\n    /**\n     * Sets the {@link AnalyticsCollector} that will collect and forward all player events.\n     *\n     * @param analyticsCollector An {@link AnalyticsCollector}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) {\n      Assertions.checkState(!buildCalled);\n      this.analyticsCollector = analyticsCollector;\n      return this;\n    }\n\n    /**\n     * Sets whether media sources should be initialized lazily.\n     *\n     * <p>If false, all initial preparation steps (e.g., manifest loads) happen immediately. If\n     * true, these initial preparations are triggered only when the player starts buffering the\n     * media.\n     *\n     * @param useLazyPreparation Whether to use lazy preparation.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public Builder setUseLazyPreparation(boolean useLazyPreparation) {\n      Assertions.checkState(!buildCalled);\n      this.useLazyPreparation = useLazyPreparation;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Clock} that will be used by the player. Should only be set for testing\n     * purposes.\n     *\n     * @param clock A {@link Clock}.\n     * @return This builder.\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    @VisibleForTesting\n    public Builder setClock(Clock clock) {\n      Assertions.checkState(!buildCalled);\n      this.clock = clock;\n      return this;\n    }\n\n    /**\n     * Builds a {@link SimpleExoPlayer} instance.\n     *\n     * @throws IllegalStateException If {@link #build()} has already been called.\n     */\n    public SimpleExoPlayer build() {\n      Assertions.checkState(!buildCalled);\n      buildCalled = true;\n      return new SimpleExoPlayer(\n          context,\n          renderersFactory,\n          trackSelector,\n          loadControl,\n          bandwidthMeter,\n          analyticsCollector,\n          clock,\n          looper);\n    }\n  }\n\n  private static final String TAG = \"SimpleExoPlayer\";\n\n  protected final Renderer[] renderers;\n\n  private final ExoPlayerImpl player;\n  private final Handler eventHandler;\n  private final ComponentListener componentListener;\n  private final CopyOnWriteArraySet<com.google.android.exoplayer2.video.VideoListener>\n      videoListeners;\n  private final CopyOnWriteArraySet<AudioListener> audioListeners;\n  private final CopyOnWriteArraySet<TextOutput> textOutputs;\n  private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;\n  private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;\n  private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;\n  private final BandwidthMeter bandwidthMeter;\n  private final AnalyticsCollector analyticsCollector;\n\n  private final AudioBecomingNoisyManager audioBecomingNoisyManager;\n  private final AudioFocusManager audioFocusManager;\n  private final WakeLockManager wakeLockManager;\n\n  @Nullable private Format videoFormat;\n  @Nullable private Format audioFormat;\n\n  @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer;\n  @Nullable private Surface surface;\n  private boolean ownsSurface;\n  private @C.VideoScalingMode int videoScalingMode;\n  @Nullable private SurfaceHolder surfaceHolder;\n  @Nullable private TextureView textureView;\n  private int surfaceWidth;\n  private int surfaceHeight;\n  @Nullable private DecoderCounters videoDecoderCounters;\n  @Nullable private DecoderCounters audioDecoderCounters;\n  private int audioSessionId;\n  private AudioAttributes audioAttributes;\n  private float audioVolume;\n  @Nullable private MediaSource mediaSource;\n  private List<Cue> currentCues;\n  @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;\n  @Nullable private CameraMotionListener cameraMotionListener;\n  private boolean hasNotifiedFullWrongThreadWarning;\n  @Nullable private PriorityTaskManager priorityTaskManager;\n  private boolean isPriorityTaskManagerRegistered;\n  private boolean playerReleased;\n\n  /**\n   * @param context A {@link Context}.\n   * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.\n   * @param trackSelector The {@link TrackSelector} that will be used by the instance.\n   * @param loadControl The {@link LoadControl} that will be used by the instance.\n   * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.\n   * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will\n   *     collect and forward all player events.\n   * @param clock The {@link Clock} that will be used by the instance. Should always be {@link\n   *     Clock#DEFAULT}, unless the player is being used from a test.\n   * @param looper The {@link Looper} which must be used for all calls to the player and which is\n   *     used to call listeners on.\n   */\n  @SuppressWarnings(\"deprecation\")\n  protected SimpleExoPlayer(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      AnalyticsCollector analyticsCollector,\n      Clock clock,\n      Looper looper) {\n    this(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        DrmSessionManager.getDummyDrmSessionManager(),\n        bandwidthMeter,\n        analyticsCollector,\n        clock,\n        looper);\n  }\n\n  /**\n   * @param context A {@link Context}.\n   * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.\n   * @param trackSelector The {@link TrackSelector} that will be used by the instance.\n   * @param loadControl The {@link LoadControl} that will be used by the instance.\n   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance\n   *     will not be used for DRM protected playbacks.\n   * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.\n   * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all\n   *     player events.\n   * @param clock The {@link Clock} that will be used by the instance. Should always be {@link\n   *     Clock#DEFAULT}, unless the player is being used from a test.\n   * @param looper The {@link Looper} which must be used for all calls to the player and which is\n   *     used to call listeners on.\n   * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl,\n   *     BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link\n   *     DrmSessionManager} to the {@link MediaSource} factories.\n   */\n  @Deprecated\n  protected SimpleExoPlayer(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      BandwidthMeter bandwidthMeter,\n      AnalyticsCollector analyticsCollector,\n      Clock clock,\n      Looper looper) {\n    this.bandwidthMeter = bandwidthMeter;\n    this.analyticsCollector = analyticsCollector;\n    componentListener = new ComponentListener();\n    videoListeners = new CopyOnWriteArraySet<>();\n    audioListeners = new CopyOnWriteArraySet<>();\n    textOutputs = new CopyOnWriteArraySet<>();\n    metadataOutputs = new CopyOnWriteArraySet<>();\n    videoDebugListeners = new CopyOnWriteArraySet<>();\n    audioDebugListeners = new CopyOnWriteArraySet<>();\n    eventHandler = new Handler(looper);\n    renderers =\n        renderersFactory.createRenderers(\n            eventHandler,\n            componentListener,\n            componentListener,\n            componentListener,\n            componentListener,\n            drmSessionManager);\n\n    // Set initial values.\n    audioVolume = 1;\n    audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n    audioAttributes = AudioAttributes.DEFAULT;\n    videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;\n    currentCues = Collections.emptyList();\n\n    // Build the player and associated objects.\n    player =\n        new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);\n    analyticsCollector.setPlayer(player);\n    addListener(analyticsCollector);\n    addListener(componentListener);\n    videoDebugListeners.add(analyticsCollector);\n    videoListeners.add(analyticsCollector);\n    audioDebugListeners.add(analyticsCollector);\n    audioListeners.add(analyticsCollector);\n    addMetadataOutput(analyticsCollector);\n    bandwidthMeter.addEventListener(eventHandler, analyticsCollector);\n    if (drmSessionManager instanceof DefaultDrmSessionManager) {\n      ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector);\n    }\n    audioBecomingNoisyManager =\n        new AudioBecomingNoisyManager(context, eventHandler, componentListener);\n    audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener);\n    wakeLockManager = new WakeLockManager(context);\n  }\n\n  @Override\n  @Nullable\n  public AudioComponent getAudioComponent() {\n    return this;\n  }\n\n  @Override\n  @Nullable\n  public VideoComponent getVideoComponent() {\n    return this;\n  }\n\n  @Override\n  @Nullable\n  public TextComponent getTextComponent() {\n    return this;\n  }\n\n  @Override\n  @Nullable\n  public MetadataComponent getMetadataComponent() {\n    return this;\n  }\n\n  /**\n   * Sets the video scaling mode.\n   *\n   * <p>Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer}\n   * is enabled and if the output surface is owned by a {@link SurfaceView}.\n   *\n   * @param videoScalingMode The video scaling mode.\n   */\n  @Override\n  public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {\n    verifyApplicationThread();\n    this.videoScalingMode = videoScalingMode;\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_SCALING_MODE)\n            .setPayload(videoScalingMode)\n            .send();\n      }\n    }\n  }\n\n  @Override\n  public @C.VideoScalingMode int getVideoScalingMode() {\n    return videoScalingMode;\n  }\n\n  @Override\n  public void clearVideoSurface() {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false);\n    maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n  }\n\n  @Override\n  public void clearVideoSurface(@Nullable Surface surface) {\n    verifyApplicationThread();\n    if (surface != null && surface == this.surface) {\n      clearVideoSurface();\n    }\n  }\n\n  @Override\n  public void setVideoSurface(@Nullable Surface surface) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    if (surface != null) {\n      clearVideoDecoderOutputBufferRenderer();\n    }\n    setVideoSurfaceInternal(surface, /* ownsSurface= */ false);\n    int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET;\n    maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize);\n  }\n\n  @Override\n  public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    if (surfaceHolder != null) {\n      clearVideoDecoderOutputBufferRenderer();\n    }\n    this.surfaceHolder = surfaceHolder;\n    if (surfaceHolder == null) {\n      setVideoSurfaceInternal(null, /* ownsSurface= */ false);\n      maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n    } else {\n      surfaceHolder.addCallback(componentListener);\n      Surface surface = surfaceHolder.getSurface();\n      if (surface != null && surface.isValid()) {\n        setVideoSurfaceInternal(surface, /* ownsSurface= */ false);\n        Rect surfaceSize = surfaceHolder.getSurfaceFrame();\n        maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());\n      } else {\n        setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false);\n        maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n      }\n    }\n  }\n\n  @Override\n  public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {\n    verifyApplicationThread();\n    if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {\n      setVideoSurfaceHolder(null);\n    }\n  }\n\n  @Override\n  public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {\n    setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());\n  }\n\n  @Override\n  public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {\n    clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());\n  }\n\n  @Override\n  public void setVideoTextureView(@Nullable TextureView textureView) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    if (textureView != null) {\n      clearVideoDecoderOutputBufferRenderer();\n    }\n    this.textureView = textureView;\n    if (textureView == null) {\n      setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);\n      maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n    } else {\n      if (textureView.getSurfaceTextureListener() != null) {\n        Log.w(TAG, \"Replacing existing SurfaceTextureListener.\");\n      }\n      textureView.setSurfaceTextureListener(componentListener);\n      SurfaceTexture surfaceTexture =\n          textureView.isAvailable() ? textureView.getSurfaceTexture() : null;\n      if (surfaceTexture == null) {\n        setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);\n        maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n      } else {\n        setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true);\n        maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight());\n      }\n    }\n  }\n\n  @Override\n  public void clearVideoTextureView(@Nullable TextureView textureView) {\n    verifyApplicationThread();\n    if (textureView != null && textureView == this.textureView) {\n      setVideoTextureView(null);\n    }\n  }\n\n  @Override\n  public void setVideoDecoderOutputBufferRenderer(\n      @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) {\n    verifyApplicationThread();\n    if (videoDecoderOutputBufferRenderer != null) {\n      clearVideoSurface();\n    }\n    setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer);\n  }\n\n  @Override\n  public void clearVideoDecoderOutputBufferRenderer() {\n    verifyApplicationThread();\n    setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null);\n  }\n\n  @Override\n  public void clearVideoDecoderOutputBufferRenderer(\n      @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) {\n    verifyApplicationThread();\n    if (videoDecoderOutputBufferRenderer != null\n        && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) {\n      clearVideoDecoderOutputBufferRenderer();\n    }\n  }\n\n  @Override\n  public void addAudioListener(AudioListener listener) {\n    audioListeners.add(listener);\n  }\n\n  @Override\n  public void removeAudioListener(AudioListener listener) {\n    audioListeners.remove(listener);\n  }\n\n  @Override\n  public void setAudioAttributes(AudioAttributes audioAttributes) {\n    setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false);\n  }\n\n  @Override\n  public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {\n    verifyApplicationThread();\n    if (playerReleased) {\n      return;\n    }\n    if (!Util.areEqual(this.audioAttributes, audioAttributes)) {\n      this.audioAttributes = audioAttributes;\n      for (Renderer renderer : renderers) {\n        if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {\n          player\n              .createMessage(renderer)\n              .setType(C.MSG_SET_AUDIO_ATTRIBUTES)\n              .setPayload(audioAttributes)\n              .send();\n        }\n      }\n      for (AudioListener audioListener : audioListeners) {\n        audioListener.onAudioAttributesChanged(audioAttributes);\n      }\n    }\n\n    @AudioFocusManager.PlayerCommand\n    int playerCommand =\n        audioFocusManager.setAudioAttributes(\n            handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState());\n    updatePlayWhenReady(getPlayWhenReady(), playerCommand);\n  }\n\n  @Override\n  public AudioAttributes getAudioAttributes() {\n    return audioAttributes;\n  }\n\n  @Override\n  public int getAudioSessionId() {\n    return audioSessionId;\n  }\n\n  @Override\n  public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {\n    verifyApplicationThread();\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_AUX_EFFECT_INFO)\n            .setPayload(auxEffectInfo)\n            .send();\n      }\n    }\n  }\n\n  @Override\n  public void clearAuxEffectInfo() {\n    setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f));\n  }\n\n  @Override\n  public void setVolume(float audioVolume) {\n    verifyApplicationThread();\n    audioVolume = Util.constrainValue(audioVolume, /* min= */ 0, /* max= */ 1);\n    if (this.audioVolume == audioVolume) {\n      return;\n    }\n    this.audioVolume = audioVolume;\n    sendVolumeToRenderers();\n    for (AudioListener audioListener : audioListeners) {\n      audioListener.onVolumeChanged(audioVolume);\n    }\n  }\n\n  @Override\n  public float getVolume() {\n    return audioVolume;\n  }\n\n  /**\n   * Sets the stream type for audio playback, used by the underlying audio track.\n   *\n   * <p>Setting the stream type during playback may introduce a short gap in audio output as the\n   * audio track is recreated. A new audio session id will also be generated.\n   *\n   * <p>Calling this method overwrites any attributes set previously by calling {@link\n   * #setAudioAttributes(AudioAttributes)}.\n   *\n   * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}.\n   * @param streamType The stream type for audio playback.\n   */\n  @Deprecated\n  public void setAudioStreamType(@C.StreamType int streamType) {\n    @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType);\n    @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType);\n    AudioAttributes audioAttributes =\n        new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build();\n    setAudioAttributes(audioAttributes);\n  }\n\n  /**\n   * Returns the stream type for audio playback.\n   *\n   * @deprecated Use {@link #getAudioAttributes()}.\n   */\n  @Deprecated\n  public @C.StreamType int getAudioStreamType() {\n    return Util.getStreamTypeForAudioUsage(audioAttributes.usage);\n  }\n\n  /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */\n  public AnalyticsCollector getAnalyticsCollector() {\n    return analyticsCollector;\n  }\n\n  /**\n   * Adds an {@link AnalyticsListener} to receive analytics events.\n   *\n   * @param listener The listener to be added.\n   */\n  public void addAnalyticsListener(AnalyticsListener listener) {\n    verifyApplicationThread();\n    analyticsCollector.addListener(listener);\n  }\n\n  /**\n   * Removes an {@link AnalyticsListener}.\n   *\n   * @param listener The listener to be removed.\n   */\n  public void removeAnalyticsListener(AnalyticsListener listener) {\n    verifyApplicationThread();\n    analyticsCollector.removeListener(listener);\n  }\n\n  /**\n   * Sets whether the player should pause automatically when audio is rerouted from a headset to\n   * device speakers. See the <a\n   * href=\"https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy\">audio\n   * becoming noisy</a> documentation for more information.\n   *\n   * <p>This feature is not enabled by default.\n   *\n   * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is\n   *     rerouted from a headset to device speakers.\n   */\n  public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) {\n    verifyApplicationThread();\n    if (playerReleased) {\n      return;\n    }\n    audioBecomingNoisyManager.setEnabled(handleAudioBecomingNoisy);\n  }\n\n  /**\n   * Sets a {@link PriorityTaskManager}, or null to clear a previously set priority task manager.\n   *\n   * <p>The priority {@link C#PRIORITY_PLAYBACK} will be set while the player is loading.\n   *\n   * @param priorityTaskManager The {@link PriorityTaskManager}, or null to clear a previously set\n   *     priority task manager.\n   */\n  public void setPriorityTaskManager(@Nullable PriorityTaskManager priorityTaskManager) {\n    verifyApplicationThread();\n    if (Util.areEqual(this.priorityTaskManager, priorityTaskManager)) {\n      return;\n    }\n    if (isPriorityTaskManagerRegistered) {\n      Assertions.checkNotNull(this.priorityTaskManager).remove(C.PRIORITY_PLAYBACK);\n    }\n    if (priorityTaskManager != null && isLoading()) {\n      priorityTaskManager.add(C.PRIORITY_PLAYBACK);\n      isPriorityTaskManagerRegistered = true;\n    } else {\n      isPriorityTaskManagerRegistered = false;\n    }\n    this.priorityTaskManager = priorityTaskManager;\n  }\n\n  /**\n   * Sets the {@link PlaybackParams} governing audio playback.\n   *\n   * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.\n   * @param params The {@link PlaybackParams}, or null to clear any previously set parameters.\n   */\n  @Deprecated\n  @TargetApi(23)\n  public void setPlaybackParams(@Nullable PlaybackParams params) {\n    PlaybackParameters playbackParameters;\n    if (params != null) {\n      params.allowDefaults();\n      playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());\n    } else {\n      playbackParameters = null;\n    }\n    setPlaybackParameters(playbackParameters);\n  }\n\n  /** Returns the video format currently being played, or null if no video is being played. */\n  @Nullable\n  public Format getVideoFormat() {\n    return videoFormat;\n  }\n\n  /** Returns the audio format currently being played, or null if no audio is being played. */\n  @Nullable\n  public Format getAudioFormat() {\n    return audioFormat;\n  }\n\n  /** Returns {@link DecoderCounters} for video, or null if no video is being played. */\n  @Nullable\n  public DecoderCounters getVideoDecoderCounters() {\n    return videoDecoderCounters;\n  }\n\n  /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */\n  @Nullable\n  public DecoderCounters getAudioDecoderCounters() {\n    return audioDecoderCounters;\n  }\n\n  @Override\n  public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {\n    videoListeners.add(listener);\n  }\n\n  @Override\n  public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {\n    videoListeners.remove(listener);\n  }\n\n  @Override\n  public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) {\n    verifyApplicationThread();\n    videoFrameMetadataListener = listener;\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER)\n            .setPayload(listener)\n            .send();\n      }\n    }\n  }\n\n  @Override\n  public void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener) {\n    verifyApplicationThread();\n    if (videoFrameMetadataListener != listener) {\n      return;\n    }\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER)\n            .setPayload(null)\n            .send();\n      }\n    }\n  }\n\n  @Override\n  public void setCameraMotionListener(CameraMotionListener listener) {\n    verifyApplicationThread();\n    cameraMotionListener = listener;\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_CAMERA_MOTION_LISTENER)\n            .setPayload(listener)\n            .send();\n      }\n    }\n  }\n\n  @Override\n  public void clearCameraMotionListener(CameraMotionListener listener) {\n    verifyApplicationThread();\n    if (cameraMotionListener != listener) {\n      return;\n    }\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_CAMERA_MOTION_LISTENER)\n            .setPayload(null)\n            .send();\n      }\n    }\n  }\n\n  /**\n   * Sets a listener to receive video events, removing all existing listeners.\n   *\n   * @param listener The listener.\n   * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void setVideoListener(VideoListener listener) {\n    videoListeners.clear();\n    if (listener != null) {\n      addVideoListener(listener);\n    }\n  }\n\n  /**\n   * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.\n   *\n   * @param listener The listener to clear.\n   * @deprecated Use {@link\n   *     #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void clearVideoListener(VideoListener listener) {\n    removeVideoListener(listener);\n  }\n\n  @Override\n  public void addTextOutput(TextOutput listener) {\n    if (!currentCues.isEmpty()) {\n      listener.onCues(currentCues);\n    }\n    textOutputs.add(listener);\n  }\n\n  @Override\n  public void removeTextOutput(TextOutput listener) {\n    textOutputs.remove(listener);\n  }\n\n  /**\n   * Sets an output to receive text events, removing all existing outputs.\n   *\n   * @param output The output.\n   * @deprecated Use {@link #addTextOutput(TextOutput)}.\n   */\n  @Deprecated\n  public void setTextOutput(TextOutput output) {\n    textOutputs.clear();\n    if (output != null) {\n      addTextOutput(output);\n    }\n  }\n\n  /**\n   * Equivalent to {@link #removeTextOutput(TextOutput)}.\n   *\n   * @param output The output to clear.\n   * @deprecated Use {@link #removeTextOutput(TextOutput)}.\n   */\n  @Deprecated\n  public void clearTextOutput(TextOutput output) {\n    removeTextOutput(output);\n  }\n\n  @Override\n  public void addMetadataOutput(MetadataOutput listener) {\n    metadataOutputs.add(listener);\n  }\n\n  @Override\n  public void removeMetadataOutput(MetadataOutput listener) {\n    metadataOutputs.remove(listener);\n  }\n\n  /**\n   * Sets an output to receive metadata events, removing all existing outputs.\n   *\n   * @param output The output.\n   * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}.\n   */\n  @Deprecated\n  public void setMetadataOutput(MetadataOutput output) {\n    metadataOutputs.retainAll(Collections.singleton(analyticsCollector));\n    if (output != null) {\n      addMetadataOutput(output);\n    }\n  }\n\n  /**\n   * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}.\n   *\n   * @param output The output to clear.\n   * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}.\n   */\n  @Deprecated\n  public void clearMetadataOutput(MetadataOutput output) {\n    removeMetadataOutput(output);\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug\n   *     information.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void setVideoDebugListener(VideoRendererEventListener listener) {\n    videoDebugListeners.retainAll(Collections.singleton(analyticsCollector));\n    if (listener != null) {\n      addVideoDebugListener(listener);\n    }\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug\n   *     information.\n   */\n  @Deprecated\n  public void addVideoDebugListener(VideoRendererEventListener listener) {\n    videoDebugListeners.add(listener);\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link\n   *     #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information.\n   */\n  @Deprecated\n  public void removeVideoDebugListener(VideoRendererEventListener listener) {\n    videoDebugListeners.remove(listener);\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug\n   *     information.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void setAudioDebugListener(AudioRendererEventListener listener) {\n    audioDebugListeners.retainAll(Collections.singleton(analyticsCollector));\n    if (listener != null) {\n      addAudioDebugListener(listener);\n    }\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug\n   *     information.\n   */\n  @Deprecated\n  public void addAudioDebugListener(AudioRendererEventListener listener) {\n    audioDebugListeners.add(listener);\n  }\n\n  /**\n   * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link\n   *     #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information.\n   */\n  @Deprecated\n  public void removeAudioDebugListener(AudioRendererEventListener listener) {\n    audioDebugListeners.remove(listener);\n  }\n\n  // ExoPlayer implementation\n\n  @Override\n  public Looper getPlaybackLooper() {\n    return player.getPlaybackLooper();\n  }\n\n  @Override\n  public Looper getApplicationLooper() {\n    return player.getApplicationLooper();\n  }\n\n  @Override\n  public void addListener(EventListener listener) {\n    verifyApplicationThread();\n    player.addListener(listener);\n  }\n\n  @Override\n  public void removeListener(EventListener listener) {\n    verifyApplicationThread();\n    player.removeListener(listener);\n  }\n\n  @Override\n  @State\n  public int getPlaybackState() {\n    verifyApplicationThread();\n    return player.getPlaybackState();\n  }\n\n  @Override\n  @PlaybackSuppressionReason\n  public int getPlaybackSuppressionReason() {\n    verifyApplicationThread();\n    return player.getPlaybackSuppressionReason();\n  }\n\n  @Override\n  @Nullable\n  public ExoPlaybackException getPlaybackError() {\n    verifyApplicationThread();\n    return player.getPlaybackError();\n  }\n\n  @Override\n  public void retry() {\n    verifyApplicationThread();\n    if (mediaSource != null\n        && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) {\n      prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);\n    }\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource) {\n    prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {\n    verifyApplicationThread();\n    if (this.mediaSource != null) {\n      this.mediaSource.removeEventListener(analyticsCollector);\n      analyticsCollector.resetForNewMediaSource();\n    }\n    this.mediaSource = mediaSource;\n    mediaSource.addEventListener(eventHandler, analyticsCollector);\n    @AudioFocusManager.PlayerCommand\n    int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());\n    updatePlayWhenReady(getPlayWhenReady(), playerCommand);\n    player.prepare(mediaSource, resetPosition, resetState);\n  }\n\n  @Override\n  public void setPlayWhenReady(boolean playWhenReady) {\n    verifyApplicationThread();\n    @AudioFocusManager.PlayerCommand\n    int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState());\n    updatePlayWhenReady(playWhenReady, playerCommand);\n  }\n\n  @Override\n  public boolean getPlayWhenReady() {\n    verifyApplicationThread();\n    return player.getPlayWhenReady();\n  }\n\n  @Override\n  public @RepeatMode int getRepeatMode() {\n    verifyApplicationThread();\n    return player.getRepeatMode();\n  }\n\n  @Override\n  public void setRepeatMode(@RepeatMode int repeatMode) {\n    verifyApplicationThread();\n    player.setRepeatMode(repeatMode);\n  }\n\n  @Override\n  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {\n    verifyApplicationThread();\n    player.setShuffleModeEnabled(shuffleModeEnabled);\n  }\n\n  @Override\n  public boolean getShuffleModeEnabled() {\n    verifyApplicationThread();\n    return player.getShuffleModeEnabled();\n  }\n\n  @Override\n  public boolean isLoading() {\n    verifyApplicationThread();\n    return player.isLoading();\n  }\n\n  @Override\n  public void seekTo(int windowIndex, long positionMs) {\n    verifyApplicationThread();\n    analyticsCollector.notifySeekStarted();\n    player.seekTo(windowIndex, positionMs);\n  }\n\n  @Override\n  public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {\n    verifyApplicationThread();\n    player.setPlaybackParameters(playbackParameters);\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    verifyApplicationThread();\n    return player.getPlaybackParameters();\n  }\n\n  @Override\n  public void setSeekParameters(@Nullable SeekParameters seekParameters) {\n    verifyApplicationThread();\n    player.setSeekParameters(seekParameters);\n  }\n\n  @Override\n  public SeekParameters getSeekParameters() {\n    verifyApplicationThread();\n    return player.getSeekParameters();\n  }\n\n  @Override\n  public void setForegroundMode(boolean foregroundMode) {\n    player.setForegroundMode(foregroundMode);\n  }\n\n  @Override\n  public void stop(boolean reset) {\n    verifyApplicationThread();\n    player.stop(reset);\n    if (mediaSource != null) {\n      mediaSource.removeEventListener(analyticsCollector);\n      analyticsCollector.resetForNewMediaSource();\n      if (reset) {\n        mediaSource = null;\n      }\n    }\n    audioFocusManager.handleStop();\n    currentCues = Collections.emptyList();\n  }\n\n  @Override\n  public void release() {\n    verifyApplicationThread();\n    audioBecomingNoisyManager.setEnabled(false);\n    audioFocusManager.handleStop();\n    wakeLockManager.setStayAwake(false);\n    player.release();\n    removeSurfaceCallbacks();\n    if (surface != null) {\n      if (ownsSurface) {\n        surface.release();\n      }\n      surface = null;\n    }\n    if (mediaSource != null) {\n      mediaSource.removeEventListener(analyticsCollector);\n      mediaSource = null;\n    }\n    if (isPriorityTaskManagerRegistered) {\n      Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);\n      isPriorityTaskManagerRegistered = false;\n    }\n    bandwidthMeter.removeEventListener(analyticsCollector);\n    currentCues = Collections.emptyList();\n    playerReleased = true;\n  }\n\n  @Override\n  public PlayerMessage createMessage(PlayerMessage.Target target) {\n    verifyApplicationThread();\n    return player.createMessage(target);\n  }\n\n  @Override\n  public int getRendererCount() {\n    verifyApplicationThread();\n    return player.getRendererCount();\n  }\n\n  @Override\n  public int getRendererType(int index) {\n    verifyApplicationThread();\n    return player.getRendererType(index);\n  }\n\n  @Override\n  public TrackGroupArray getCurrentTrackGroups() {\n    verifyApplicationThread();\n    return player.getCurrentTrackGroups();\n  }\n\n  @Override\n  public TrackSelectionArray getCurrentTrackSelections() {\n    verifyApplicationThread();\n    return player.getCurrentTrackSelections();\n  }\n\n  @Override\n  public Timeline getCurrentTimeline() {\n    verifyApplicationThread();\n    return player.getCurrentTimeline();\n  }\n\n  @Override\n  public int getCurrentPeriodIndex() {\n    verifyApplicationThread();\n    return player.getCurrentPeriodIndex();\n  }\n\n  @Override\n  public int getCurrentWindowIndex() {\n    verifyApplicationThread();\n    return player.getCurrentWindowIndex();\n  }\n\n  @Override\n  public long getDuration() {\n    verifyApplicationThread();\n    return player.getDuration();\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    verifyApplicationThread();\n    return player.getCurrentPosition();\n  }\n\n  @Override\n  public long getBufferedPosition() {\n    verifyApplicationThread();\n    return player.getBufferedPosition();\n  }\n\n  @Override\n  public long getTotalBufferedDuration() {\n    verifyApplicationThread();\n    return player.getTotalBufferedDuration();\n  }\n\n  @Override\n  public boolean isPlayingAd() {\n    verifyApplicationThread();\n    return player.isPlayingAd();\n  }\n\n  @Override\n  public int getCurrentAdGroupIndex() {\n    verifyApplicationThread();\n    return player.getCurrentAdGroupIndex();\n  }\n\n  @Override\n  public int getCurrentAdIndexInAdGroup() {\n    verifyApplicationThread();\n    return player.getCurrentAdIndexInAdGroup();\n  }\n\n  @Override\n  public long getContentPosition() {\n    verifyApplicationThread();\n    return player.getContentPosition();\n  }\n\n  @Override\n  public long getContentBufferedPosition() {\n    verifyApplicationThread();\n    return player.getContentBufferedPosition();\n  }\n\n  /**\n   * Sets whether the player should use a {@link android.os.PowerManager.WakeLock} to ensure the\n   * device stays awake for playback, even when the screen is off.\n   *\n   * <p>Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission.\n   * It should be used together with a foreground {@link android.app.Service} for use cases where\n   * playback can occur when the screen is off (e.g. background audio playback). It is not useful if\n   * the screen will always be on during playback (e.g. foreground video playback).\n   *\n   * <p>This feature is not enabled by default. If enabled, a WakeLock is held whenever the player\n   * is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code\n   * playWhenReady = true}.\n   *\n   * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock}\n   *     to ensure the device stays awake for playback, even when the screen is off.\n   */\n  public void setHandleWakeLock(boolean handleWakeLock) {\n    wakeLockManager.setEnabled(handleWakeLock);\n  }\n\n  // Internal methods.\n\n  private void removeSurfaceCallbacks() {\n    if (textureView != null) {\n      if (textureView.getSurfaceTextureListener() != componentListener) {\n        Log.w(TAG, \"SurfaceTextureListener already unset or replaced.\");\n      } else {\n        textureView.setSurfaceTextureListener(null);\n      }\n      textureView = null;\n    }\n    if (surfaceHolder != null) {\n      surfaceHolder.removeCallback(componentListener);\n      surfaceHolder = null;\n    }\n  }\n\n  private void setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurface) {\n    // Note: We don't turn this method into a no-op if the surface is being replaced with itself\n    // so as to ensure onRenderedFirstFrame callbacks are still called in this case.\n    List<PlayerMessage> messages = new ArrayList<>();\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {\n        messages.add(\n            player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send());\n      }\n    }\n    if (this.surface != null && this.surface != surface) {\n      // We're replacing a surface. Block to ensure that it's not accessed after the method returns.\n      try {\n        for (PlayerMessage message : messages) {\n          message.blockUntilDelivered();\n        }\n      } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n      }\n      // If we created the previous surface, we are responsible for releasing it.\n      if (this.ownsSurface) {\n        this.surface.release();\n      }\n    }\n    this.surface = surface;\n    this.ownsSurface = ownsSurface;\n  }\n\n  private void setVideoDecoderOutputBufferRendererInternal(\n      @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) {\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {\n        player\n            .createMessage(renderer)\n            .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER)\n            .setPayload(videoDecoderOutputBufferRenderer)\n            .send();\n      }\n    }\n    this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer;\n  }\n\n  private void maybeNotifySurfaceSizeChanged(int width, int height) {\n    if (width != surfaceWidth || height != surfaceHeight) {\n      surfaceWidth = width;\n      surfaceHeight = height;\n      for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {\n        videoListener.onSurfaceSizeChanged(width, height);\n      }\n    }\n  }\n\n  private void sendVolumeToRenderers() {\n    float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier();\n    for (Renderer renderer : renderers) {\n      if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {\n        player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send();\n      }\n    }\n  }\n\n  private void updatePlayWhenReady(\n      boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {\n    playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;\n    @PlaybackSuppressionReason\n    int playbackSuppressionReason =\n        playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY\n            ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS\n            : Player.PLAYBACK_SUPPRESSION_REASON_NONE;\n    player.setPlayWhenReady(playWhenReady, playbackSuppressionReason);\n  }\n\n  private void verifyApplicationThread() {\n    if (Looper.myLooper() != getApplicationLooper()) {\n      Log.w(\n          TAG,\n          \"Player is accessed on the wrong thread. See \"\n              + \"https://exoplayer.dev/issues/player-accessed-on-wrong-thread\",\n          hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());\n      hasNotifiedFullWrongThreadWarning = true;\n    }\n  }\n\n  private final class ComponentListener\n      implements VideoRendererEventListener,\n          AudioRendererEventListener,\n          TextOutput,\n          MetadataOutput,\n          SurfaceHolder.Callback,\n          TextureView.SurfaceTextureListener,\n          AudioFocusManager.PlayerControl,\n          AudioBecomingNoisyManager.EventListener,\n          EventListener {\n\n    // VideoRendererEventListener implementation\n\n    @Override\n    public void onVideoEnabled(DecoderCounters counters) {\n      videoDecoderCounters = counters;\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoEnabled(counters);\n      }\n    }\n\n    @Override\n    public void onVideoDecoderInitialized(\n        String decoderName, long initializedTimestampMs, long initializationDurationMs) {\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoDecoderInitialized(\n            decoderName, initializedTimestampMs, initializationDurationMs);\n      }\n    }\n\n    @Override\n    public void onVideoInputFormatChanged(Format format) {\n      videoFormat = format;\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoInputFormatChanged(format);\n      }\n    }\n\n    @Override\n    public void onDroppedFrames(int count, long elapsed) {\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onDroppedFrames(count, elapsed);\n      }\n    }\n\n    @Override\n    public void onVideoSizeChanged(\n        int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n      for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {\n        // Prevent duplicate notification if a listener is both a VideoRendererEventListener and\n        // a VideoListener, as they have the same method signature.\n        if (!videoDebugListeners.contains(videoListener)) {\n          videoListener.onVideoSizeChanged(\n              width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n        }\n      }\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoSizeChanged(\n            width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n      }\n    }\n\n    @Override\n    public void onRenderedFirstFrame(Surface surface) {\n      if (SimpleExoPlayer.this.surface == surface) {\n        for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {\n          videoListener.onRenderedFirstFrame();\n        }\n      }\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onRenderedFirstFrame(surface);\n      }\n    }\n\n    @Override\n    public void onVideoDisabled(DecoderCounters counters) {\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoDisabled(counters);\n      }\n      videoFormat = null;\n      videoDecoderCounters = null;\n    }\n\n    // AudioRendererEventListener implementation\n\n    @Override\n    public void onAudioEnabled(DecoderCounters counters) {\n      audioDecoderCounters = counters;\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioEnabled(counters);\n      }\n    }\n\n    @Override\n    public void onAudioSessionId(int sessionId) {\n      if (audioSessionId == sessionId) {\n        return;\n      }\n      audioSessionId = sessionId;\n      for (AudioListener audioListener : audioListeners) {\n        // Prevent duplicate notification if a listener is both a AudioRendererEventListener and\n        // a AudioListener, as they have the same method signature.\n        if (!audioDebugListeners.contains(audioListener)) {\n          audioListener.onAudioSessionId(sessionId);\n        }\n      }\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioSessionId(sessionId);\n      }\n    }\n\n    @Override\n    public void onAudioDecoderInitialized(\n        String decoderName, long initializedTimestampMs, long initializationDurationMs) {\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioDecoderInitialized(\n            decoderName, initializedTimestampMs, initializationDurationMs);\n      }\n    }\n\n    @Override\n    public void onAudioInputFormatChanged(Format format) {\n      audioFormat = format;\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioInputFormatChanged(format);\n      }\n    }\n\n    @Override\n    public void onAudioSinkUnderrun(\n        int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n      }\n    }\n\n    @Override\n    public void onAudioDisabled(DecoderCounters counters) {\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioDisabled(counters);\n      }\n      audioFormat = null;\n      audioDecoderCounters = null;\n      audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n    }\n\n    // TextOutput implementation\n\n    @Override\n    public void onCues(List<Cue> cues) {\n      currentCues = cues;\n      for (TextOutput textOutput : textOutputs) {\n        textOutput.onCues(cues);\n      }\n    }\n\n    // MetadataOutput implementation\n\n    @Override\n    public void onMetadata(Metadata metadata) {\n      for (MetadataOutput metadataOutput : metadataOutputs) {\n        metadataOutput.onMetadata(metadata);\n      }\n    }\n\n    // SurfaceHolder.Callback implementation\n\n    @Override\n    public void surfaceCreated(SurfaceHolder holder) {\n      setVideoSurfaceInternal(holder.getSurface(), false);\n    }\n\n    @Override\n    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {\n      maybeNotifySurfaceSizeChanged(width, height);\n    }\n\n    @Override\n    public void surfaceDestroyed(SurfaceHolder holder) {\n      setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false);\n      maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n    }\n\n    // TextureView.SurfaceTextureListener implementation\n\n    @Override\n    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {\n      setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true);\n      maybeNotifySurfaceSizeChanged(width, height);\n    }\n\n    @Override\n    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {\n      maybeNotifySurfaceSizeChanged(width, height);\n    }\n\n    @Override\n    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {\n      setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);\n      maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);\n      return true;\n    }\n\n    @Override\n    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {\n      // Do nothing.\n    }\n\n    // AudioFocusManager.PlayerControl implementation\n\n    @Override\n    public void setVolumeMultiplier(float volumeMultiplier) {\n      sendVolumeToRenderers();\n    }\n\n    @Override\n    public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) {\n      updatePlayWhenReady(getPlayWhenReady(), playerCommand);\n    }\n\n    // AudioBecomingNoisyManager.EventListener implementation.\n\n    @Override\n    public void onAudioBecomingNoisy() {\n      setPlayWhenReady(false);\n    }\n\n    // Player.EventListener implementation.\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n      if (priorityTaskManager != null) {\n        if (isLoading && !isPriorityTaskManagerRegistered) {\n          priorityTaskManager.add(C.PRIORITY_PLAYBACK);\n          isPriorityTaskManagerRegistered = true;\n        } else if (!isLoading && isPriorityTaskManagerRegistered) {\n          priorityTaskManager.remove(C.PRIORITY_PLAYBACK);\n          isPriorityTaskManagerRegistered = false;\n        }\n      }\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {\n      switch (playbackState) {\n        case Player.STATE_READY:\n        case Player.STATE_BUFFERING:\n          wakeLockManager.setStayAwake(playWhenReady);\n          break;\n        case Player.STATE_ENDED:\n        case Player.STATE_IDLE:\n          wakeLockManager.setStayAwake(false);\n          break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/Timeline.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * A flexible representation of the structure of media. A timeline is able to represent the\n * structure of a wide variety of media, from simple cases like a single media file through to\n * complex compositions of media such as playlists and streams with inserted ads. Instances are\n * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides\n * a snapshot of the current state.\n *\n * <p>A timeline consists of {@link Window Windows} and {@link Period Periods}.\n *\n * <ul>\n *   <li>A {@link Window} usually corresponds to one playlist item. It may span one or more periods\n *       and it defines the region within those periods that's currently available for playback. The\n *       window also provides additional information such as whether seeking is supported within the\n *       window and the default position, which is the position from which playback will start when\n *       the player starts playing the window.\n *   <li>A {@link Period} defines a single logical piece of media, for example a media file. It may\n *       also define groups of ads inserted into the media, along with information about whether\n *       those ads have been loaded and played.\n * </ul>\n *\n * <p>The following examples illustrate timelines for various use cases.\n *\n * <h3 id=\"single-file\">Single media file or on-demand stream</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-single-file.svg\" alt=\"Example timeline for a\n * single file\"> A timeline for a single media file or on-demand stream consists of a single period\n * and window. The window spans the whole period, indicating that all parts of the media are\n * available for playback. The window's default position is typically at the start of the period\n * (indicated by the black dot in the figure above).\n *\n * <h3>Playlist of media files or on-demand streams</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-playlist.svg\" alt=\"Example timeline for a playlist\n * of files\"> A timeline for a playlist of media files or on-demand streams consists of multiple\n * periods, each with its own window. Each window spans the whole of the corresponding period, and\n * typically has a default position at the start of the period. The properties of the periods and\n * windows (e.g. their durations and whether the window is seekable) will often only become known\n * when the player starts buffering the corresponding file or stream.\n *\n * <h3 id=\"live-limited\">Live stream with limited availability</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-live-limited.svg\" alt=\"Example timeline for a live\n * stream with limited availability\"> A timeline for a live stream consists of a period whose\n * duration is unknown, since it's continually extending as more content is broadcast. If content\n * only remains available for a limited period of time then the window may start at a non-zero\n * position, defining the region of content that can still be played. The window will have {@link\n * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to\n * true as long as we expect changes to the live window. Its default position is typically near to\n * the live edge (indicated by the black dot in the figure above).\n *\n * <h3>Live stream with indefinite availability</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-live-indefinite.svg\" alt=\"Example timeline for a\n * live stream with indefinite availability\"> A timeline for a live stream with indefinite\n * availability is similar to the <a href=\"#live-limited\">Live stream with limited availability</a>\n * case, except that the window starts at the beginning of the period to indicate that all of the\n * previously broadcast content can still be played.\n *\n * <h3 id=\"live-multi-period\">Live stream with multiple periods</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-live-multi-period.svg\" alt=\"Example timeline for a\n * live stream with multiple periods\"> This case arises when a live stream is explicitly divided\n * into separate periods, for example at content boundaries. This case is similar to the <a\n * href=\"#live-limited\">Live stream with limited availability</a> case, except that the window may\n * span more than one period. Multiple periods are also possible in the indefinite availability\n * case.\n *\n * <h3>On-demand stream followed by live stream</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-advanced.svg\" alt=\"Example timeline for an\n * on-demand stream followed by a live stream\"> This case is the concatenation of the <a\n * href=\"#single-file\">Single media file or on-demand stream</a> and <a href=\"#multi-period\">Live\n * stream with multiple periods</a> cases. When playback of the on-demand stream ends, playback of\n * the live stream will start from its default position near the live edge.\n *\n * <h3 id=\"single-file-midrolls\">On-demand stream with mid-roll ads</h3>\n *\n * <p align=\"center\"><img src=\"doc-files/timeline-single-file-midrolls.svg\" alt=\"Example timeline\n * for an on-demand stream with mid-roll ad groups\"> This case includes mid-roll ad groups, which\n * are defined as part of the timeline's single period. The period can be queried for information\n * about the ad groups and the ads they contain.\n */\npublic abstract class Timeline {\n\n  /**\n   * Holds information about a window in a {@link Timeline}. A window usually corresponds to one\n   * playlist item and defines a region of media currently available for playback along with\n   * additional information such as whether seeking is supported within the window. The figure below\n   * shows some of the information defined by a window, as well as how this information relates to\n   * corresponding {@link Period Periods} in the timeline.\n   *\n   * <p align=\"center\"><img src=\"doc-files/timeline-window.svg\" alt=\"Information defined by a\n   * timeline window\">\n   */\n  public static final class Window {\n\n    /**\n     * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}.\n     */\n    public static final Object SINGLE_WINDOW_UID = new Object();\n\n    /**\n     * A unique identifier for the window. Single-window {@link Timeline Timelines} must use {@link\n     * #SINGLE_WINDOW_UID}.\n     */\n    public Object uid;\n\n    /** A tag for the window. Not necessarily unique. */\n    @Nullable public Object tag;\n\n    /** The manifest of the window. May be {@code null}. */\n    @Nullable public Object manifest;\n\n    /**\n     * The start time of the presentation to which this window belongs in milliseconds since the\n     * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only.\n     */\n    public long presentationStartTimeMs;\n\n    /**\n     * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown\n     * or not applicable. For informational purposes only.\n     */\n    public long windowStartTimeMs;\n\n    /**\n     * Whether it's possible to seek within this window.\n     */\n    public boolean isSeekable;\n\n    // TODO: Split this to better describe which parts of the window might change. For example it\n    // should be possible to individually determine whether the start and end positions of the\n    // window may change relative to the underlying periods. For an example of where it's useful to\n    // know that the end position is fixed whilst the start position may still change, see:\n    // https://github.com/google/ExoPlayer/issues/4780.\n    /** Whether this window may change when the timeline is updated. */\n    public boolean isDynamic;\n\n    /**\n     * Whether the media in this window is live. For informational purposes only.\n     *\n     * <p>Check {@link #isDynamic} to know whether this window may still change.\n     */\n    public boolean isLive;\n\n    /** The index of the first period that belongs to this window. */\n    public int firstPeriodIndex;\n\n    /**\n     * The index of the last period that belongs to this window.\n     */\n    public int lastPeriodIndex;\n\n    /**\n     * The default position relative to the start of the window at which to begin playback, in\n     * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a\n     * non-zero default position projection, and if the specified projection cannot be performed\n     * whilst remaining within the bounds of the window.\n     */\n    public long defaultPositionUs;\n\n    /**\n     * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long durationUs;\n\n    /**\n     * The position of the start of this window relative to the start of the first period belonging\n     * to it, in microseconds.\n     */\n    public long positionInFirstPeriodUs;\n\n    /** Creates window. */\n    public Window() {\n      uid = SINGLE_WINDOW_UID;\n    }\n\n    /** Sets the data held by this window. */\n    public Window set(\n        Object uid,\n        @Nullable Object tag,\n        @Nullable Object manifest,\n        long presentationStartTimeMs,\n        long windowStartTimeMs,\n        boolean isSeekable,\n        boolean isDynamic,\n        boolean isLive,\n        long defaultPositionUs,\n        long durationUs,\n        int firstPeriodIndex,\n        int lastPeriodIndex,\n        long positionInFirstPeriodUs) {\n      this.uid = uid;\n      this.tag = tag;\n      this.manifest = manifest;\n      this.presentationStartTimeMs = presentationStartTimeMs;\n      this.windowStartTimeMs = windowStartTimeMs;\n      this.isSeekable = isSeekable;\n      this.isDynamic = isDynamic;\n      this.isLive = isLive;\n      this.defaultPositionUs = defaultPositionUs;\n      this.durationUs = durationUs;\n      this.firstPeriodIndex = firstPeriodIndex;\n      this.lastPeriodIndex = lastPeriodIndex;\n      this.positionInFirstPeriodUs = positionInFirstPeriodUs;\n      return this;\n    }\n\n    /**\n     * Returns the default position relative to the start of the window at which to begin playback,\n     * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a\n     * non-zero default position projection, and if the specified projection cannot be performed\n     * whilst remaining within the bounds of the window.\n     */\n    public long getDefaultPositionMs() {\n      return C.usToMs(defaultPositionUs);\n    }\n\n    /**\n     * Returns the default position relative to the start of the window at which to begin playback,\n     * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a\n     * non-zero default position projection, and if the specified projection cannot be performed\n     * whilst remaining within the bounds of the window.\n     */\n    public long getDefaultPositionUs() {\n      return defaultPositionUs;\n    }\n\n    /**\n     * Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long getDurationMs() {\n      return C.usToMs(durationUs);\n    }\n\n    /**\n     * Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long getDurationUs() {\n      return durationUs;\n    }\n\n    /**\n     * Returns the position of the start of this window relative to the start of the first period\n     * belonging to it, in milliseconds.\n     */\n    public long getPositionInFirstPeriodMs() {\n      return C.usToMs(positionInFirstPeriodUs);\n    }\n\n    /**\n     * Returns the position of the start of this window relative to the start of the first period\n     * belonging to it, in microseconds.\n     */\n    public long getPositionInFirstPeriodUs() {\n      return positionInFirstPeriodUs;\n    }\n\n  }\n\n  /**\n   * Holds information about a period in a {@link Timeline}. A period defines a single logical piece\n   * of media, for example a media file. It may also define groups of ads inserted into the media,\n   * along with information about whether those ads have been loaded and played.\n   * <p>\n   * The figure below shows some of the information defined by a period, as well as how this\n   * information relates to a corresponding {@link Window} in the timeline.\n   * <p align=\"center\">\n   *   <img src=\"doc-files/timeline-period.svg\" alt=\"Information defined by a period\">\n   * </p>\n   */\n  public static final class Period {\n\n    /**\n     * An identifier for the period. Not necessarily unique. May be null if the ids of the period\n     * are not required.\n     */\n    @Nullable public Object id;\n\n    /**\n     * A unique identifier for the period. May be null if the ids of the period are not required.\n     */\n    @Nullable public Object uid;\n\n    /**\n     * The index of the window to which this period belongs.\n     */\n    public int windowIndex;\n\n    /**\n     * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long durationUs;\n\n    private long positionInWindowUs;\n    private AdPlaybackState adPlaybackState;\n\n    /** Creates a new instance with no ad playback state. */\n    public Period() {\n      adPlaybackState = AdPlaybackState.NONE;\n    }\n\n    /**\n     * Sets the data held by this period.\n     *\n     * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the\n     *     period are not required.\n     * @param uid A unique identifier for the period. May be null if the ids of the period are not\n     *     required.\n     * @param windowIndex The index of the window to which this period belongs.\n     * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if\n     *     unknown.\n     * @param positionInWindowUs The position of the start of this period relative to the start of\n     *     the window to which it belongs, in milliseconds. May be negative if the start of the\n     *     period is not within the window.\n     * @return This period, for convenience.\n     */\n    public Period set(\n        @Nullable Object id,\n        @Nullable Object uid,\n        int windowIndex,\n        long durationUs,\n        long positionInWindowUs) {\n      return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE);\n    }\n\n    /**\n     * Sets the data held by this period.\n     *\n     * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the\n     *     period are not required.\n     * @param uid A unique identifier for the period. May be null if the ids of the period are not\n     *     required.\n     * @param windowIndex The index of the window to which this period belongs.\n     * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if\n     *     unknown.\n     * @param positionInWindowUs The position of the start of this period relative to the start of\n     *     the window to which it belongs, in milliseconds. May be negative if the start of the\n     *     period is not within the window.\n     * @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if\n     *     there are no ads.\n     * @return This period, for convenience.\n     */\n    public Period set(\n        @Nullable Object id,\n        @Nullable Object uid,\n        int windowIndex,\n        long durationUs,\n        long positionInWindowUs,\n        AdPlaybackState adPlaybackState) {\n      this.id = id;\n      this.uid = uid;\n      this.windowIndex = windowIndex;\n      this.durationUs = durationUs;\n      this.positionInWindowUs = positionInWindowUs;\n      this.adPlaybackState = adPlaybackState;\n      return this;\n    }\n\n    /**\n     * Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long getDurationMs() {\n      return C.usToMs(durationUs);\n    }\n\n    /**\n     * Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public long getDurationUs() {\n      return durationUs;\n    }\n\n    /**\n     * Returns the position of the start of this period relative to the start of the window to which\n     * it belongs, in milliseconds. May be negative if the start of the period is not within the\n     * window.\n     */\n    public long getPositionInWindowMs() {\n      return C.usToMs(positionInWindowUs);\n    }\n\n    /**\n     * Returns the position of the start of this period relative to the start of the window to which\n     * it belongs, in microseconds. May be negative if the start of the period is not within the\n     * window.\n     */\n    public long getPositionInWindowUs() {\n      return positionInWindowUs;\n    }\n\n    /**\n     * Returns the number of ad groups in the period.\n     */\n    public int getAdGroupCount() {\n      return adPlaybackState.adGroupCount;\n    }\n\n    /**\n     * Returns the time of the ad group at index {@code adGroupIndex} in the period, in\n     * microseconds.\n     *\n     * @param adGroupIndex The ad group index.\n     * @return The time of the ad group at the index, in microseconds, or {@link\n     *     C#TIME_END_OF_SOURCE} for a post-roll ad group.\n     */\n    public long getAdGroupTimeUs(int adGroupIndex) {\n      return adPlaybackState.adGroupTimesUs[adGroupIndex];\n    }\n\n    /**\n     * Returns the index of the first ad in the specified ad group that should be played, or the\n     * number of ads in the ad group if no ads should be played.\n     *\n     * @param adGroupIndex The ad group index.\n     * @return The index of the first ad that should be played, or the number of ads in the ad group\n     *     if no ads should be played.\n     */\n    public int getFirstAdIndexToPlay(int adGroupIndex) {\n      return adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();\n    }\n\n    /**\n     * Returns the index of the next ad in the specified ad group that should be played after\n     * playing {@code adIndexInAdGroup}, or the number of ads in the ad group if no later ads should\n     * be played.\n     *\n     * @param adGroupIndex The ad group index.\n     * @param lastPlayedAdIndex The last played ad index in the ad group.\n     * @return The index of the next ad that should be played, or the number of ads in the ad group\n     *     if the ad group does not have any ads remaining to play.\n     */\n    public int getNextAdIndexToPlay(int adGroupIndex, int lastPlayedAdIndex) {\n      return adPlaybackState.adGroups[adGroupIndex].getNextAdIndexToPlay(lastPlayedAdIndex);\n    }\n\n    /**\n     * Returns whether the ad group at index {@code adGroupIndex} has been played.\n     *\n     * @param adGroupIndex The ad group index.\n     * @return Whether the ad group at index {@code adGroupIndex} has been played.\n     */\n    public boolean hasPlayedAdGroup(int adGroupIndex) {\n      return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds();\n    }\n\n    /**\n     * Returns the index of the ad group at or before {@code positionUs}, if that ad group is\n     * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has\n     * no ads remaining to be played, or if there is no such ad group.\n     *\n     * @param positionUs The position at or before which to find an ad group, in microseconds.\n     * @return The index of the ad group, or {@link C#INDEX_UNSET}.\n     */\n    public int getAdGroupIndexForPositionUs(long positionUs) {\n      return adPlaybackState.getAdGroupIndexForPositionUs(positionUs);\n    }\n\n    /**\n     * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be\n     * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.\n     *\n     * @param positionUs The position after which to find an ad group, in microseconds.\n     * @return The index of the ad group, or {@link C#INDEX_UNSET}.\n     */\n    public int getAdGroupIndexAfterPositionUs(long positionUs) {\n      return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs, durationUs);\n    }\n\n    /**\n     * Returns the number of ads in the ad group at index {@code adGroupIndex}, or\n     * {@link C#LENGTH_UNSET} if not yet known.\n     *\n     * @param adGroupIndex The ad group index.\n     * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known.\n     */\n    public int getAdCountInAdGroup(int adGroupIndex) {\n      return adPlaybackState.adGroups[adGroupIndex].count;\n    }\n\n    /**\n     * Returns whether the URL for the specified ad is known.\n     *\n     * @param adGroupIndex The ad group index.\n     * @param adIndexInAdGroup The ad index in the ad group.\n     * @return Whether the URL for the specified ad is known.\n     */\n    public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {\n      AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];\n      return adGroup.count != C.LENGTH_UNSET\n          && adGroup.states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE;\n    }\n\n    /**\n     * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at\n     * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known.\n     *\n     * @param adGroupIndex The ad group index.\n     * @param adIndexInAdGroup The ad index in the ad group.\n     * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known.\n     */\n    public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) {\n      AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];\n      return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET;\n    }\n\n    /**\n     * Returns the position offset in the first unplayed ad at which to begin playback, in\n     * microseconds.\n     */\n    public long getAdResumePositionUs() {\n      return adPlaybackState.adResumePositionUs;\n    }\n\n  }\n\n  /** An empty timeline. */\n  public static final Timeline EMPTY =\n      new Timeline() {\n\n        @Override\n        public int getWindowCount() {\n          return 0;\n        }\n\n        @Override\n        public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n          throw new IndexOutOfBoundsException();\n        }\n\n        @Override\n        public int getPeriodCount() {\n          return 0;\n        }\n\n        @Override\n        public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n          throw new IndexOutOfBoundsException();\n        }\n\n        @Override\n        public int getIndexOfPeriod(Object uid) {\n          return C.INDEX_UNSET;\n        }\n\n        @Override\n        public Object getUidOfPeriod(int periodIndex) {\n          throw new IndexOutOfBoundsException();\n        }\n      };\n\n  /**\n   * Returns whether the timeline is empty.\n   */\n  public final boolean isEmpty() {\n    return getWindowCount() == 0;\n  }\n\n  /**\n   * Returns the number of windows in the timeline.\n   */\n  public abstract int getWindowCount();\n\n  /**\n   * Returns the index of the window after the window at index {@code windowIndex} depending on the\n   * {@code repeatMode} and whether shuffling is enabled.\n   *\n   * @param windowIndex Index of a window in the timeline.\n   * @param repeatMode A repeat mode.\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.\n   */\n  public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled) {\n    switch (repeatMode) {\n      case Player.REPEAT_MODE_OFF:\n        return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET\n            : windowIndex + 1;\n      case Player.REPEAT_MODE_ONE:\n        return windowIndex;\n      case Player.REPEAT_MODE_ALL:\n        return windowIndex == getLastWindowIndex(shuffleModeEnabled)\n            ? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Returns the index of the window before the window at index {@code windowIndex} depending on the\n   * {@code repeatMode} and whether shuffling is enabled.\n   *\n   * @param windowIndex Index of a window in the timeline.\n   * @param repeatMode A repeat mode.\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.\n   */\n  public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled) {\n    switch (repeatMode) {\n      case Player.REPEAT_MODE_OFF:\n        return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET\n            : windowIndex - 1;\n      case Player.REPEAT_MODE_ONE:\n        return windowIndex;\n      case Player.REPEAT_MODE_ALL:\n        return windowIndex == getFirstWindowIndex(shuffleModeEnabled)\n            ? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Returns the index of the last window in the playback order depending on whether shuffling is\n   * enabled.\n   *\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the\n   *     timeline is empty.\n   */\n  public int getLastWindowIndex(boolean shuffleModeEnabled) {\n    return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1;\n  }\n\n  /**\n   * Returns the index of the first window in the playback order depending on whether shuffling is\n   * enabled.\n   *\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the\n   *     timeline is empty.\n   */\n  public int getFirstWindowIndex(boolean shuffleModeEnabled) {\n    return isEmpty() ? C.INDEX_UNSET : 0;\n  }\n\n  /**\n   * Populates a {@link Window} with data for the window at the specified index.\n   *\n   * @param windowIndex The index of the window.\n   * @param window The {@link Window} to populate. Must not be null.\n   * @return The populated {@link Window}, for convenience.\n   */\n  public final Window getWindow(int windowIndex, Window window) {\n    return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0);\n  }\n\n  /** @deprecated Use {@link #getWindow(int, Window)} instead. Tags will always be set. */\n  @Deprecated\n  public final Window getWindow(int windowIndex, Window window, boolean setTag) {\n    return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0);\n  }\n\n  /**\n   * Populates a {@link Window} with data for the window at the specified index.\n   *\n   * @param windowIndex The index of the window.\n   * @param window The {@link Window} to populate. Must not be null.\n   * @param defaultPositionProjectionUs A duration into the future that the populated window's\n   *     default start position should be projected.\n   * @return The populated {@link Window}, for convenience.\n   */\n  public abstract Window getWindow(\n      int windowIndex, Window window, long defaultPositionProjectionUs);\n\n  /**\n   * Returns the number of periods in the timeline.\n   */\n  public abstract int getPeriodCount();\n\n  /**\n   * Returns the index of the period after the period at index {@code periodIndex} depending on the\n   * {@code repeatMode} and whether shuffling is enabled.\n   *\n   * @param periodIndex Index of a period in the timeline.\n   * @param period A {@link Period} to be used internally. Must not be null.\n   * @param window A {@link Window} to be used internally. Must not be null.\n   * @param repeatMode A repeat mode.\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period.\n   */\n  public final int getNextPeriodIndex(int periodIndex, Period period, Window window,\n      @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {\n    int windowIndex = getPeriod(periodIndex, period).windowIndex;\n    if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {\n      int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);\n      if (nextWindowIndex == C.INDEX_UNSET) {\n        return C.INDEX_UNSET;\n      }\n      return getWindow(nextWindowIndex, window).firstPeriodIndex;\n    }\n    return periodIndex + 1;\n  }\n\n  /**\n   * Returns whether the given period is the last period of the timeline depending on the\n   * {@code repeatMode} and whether shuffling is enabled.\n   *\n   * @param periodIndex A period index.\n   * @param period A {@link Period} to be used internally. Must not be null.\n   * @param window A {@link Window} to be used internally. Must not be null.\n   * @param repeatMode A repeat mode.\n   * @param shuffleModeEnabled Whether shuffling is enabled.\n   * @return Whether the period of the given index is the last period of the timeline.\n   */\n  public final boolean isLastPeriod(int periodIndex, Period period, Window window,\n      @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {\n    return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled)\n        == C.INDEX_UNSET;\n  }\n\n  /**\n   * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position\n   * projection.\n   */\n  public final Pair<Object, Long> getPeriodPosition(\n      Window window, Period period, int windowIndex, long windowPositionUs) {\n    return Assertions.checkNotNull(\n        getPeriodPosition(\n            window, period, windowIndex, windowPositionUs, /* defaultPositionProjectionUs= */ 0));\n  }\n\n  /**\n   * Converts (windowIndex, windowPositionUs) to the corresponding (periodUid, periodPositionUs).\n   *\n   * @param window A {@link Window} that may be overwritten.\n   * @param period A {@link Period} that may be overwritten.\n   * @param windowIndex The window index.\n   * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default\n   *     start position.\n   * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the\n   *     duration into the future by which the window's position should be projected.\n   * @return The corresponding (periodUid, periodPositionUs), or null if {@code #windowPositionUs}\n   *     is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's\n   *     position could not be projected by {@code defaultPositionProjectionUs}.\n   */\n  @Nullable\n  public final Pair<Object, Long> getPeriodPosition(\n      Window window,\n      Period period,\n      int windowIndex,\n      long windowPositionUs,\n      long defaultPositionProjectionUs) {\n    Assertions.checkIndex(windowIndex, 0, getWindowCount());\n    getWindow(windowIndex, window, defaultPositionProjectionUs);\n    if (windowPositionUs == C.TIME_UNSET) {\n      windowPositionUs = window.getDefaultPositionUs();\n      if (windowPositionUs == C.TIME_UNSET) {\n        return null;\n      }\n    }\n    int periodIndex = window.firstPeriodIndex;\n    long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;\n    long periodDurationUs = getPeriod(periodIndex, period, /* setIds= */ true).getDurationUs();\n    while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs\n        && periodIndex < window.lastPeriodIndex) {\n      periodPositionUs -= periodDurationUs;\n      periodDurationUs = getPeriod(++periodIndex, period, /* setIds= */ true).getDurationUs();\n    }\n    return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs);\n  }\n\n  /**\n   * Populates a {@link Period} with data for the period with the specified unique identifier.\n   *\n   * @param periodUid The unique identifier of the period.\n   * @param period The {@link Period} to populate. Must not be null.\n   * @return The populated {@link Period}, for convenience.\n   */\n  public Period getPeriodByUid(Object periodUid, Period period) {\n    return getPeriod(getIndexOfPeriod(periodUid), period, /* setIds= */ true);\n  }\n\n  /**\n   * Populates a {@link Period} with data for the period at the specified index. {@link Period#id}\n   * and {@link Period#uid} will be set to null.\n   *\n   * @param periodIndex The index of the period.\n   * @param period The {@link Period} to populate. Must not be null.\n   * @return The populated {@link Period}, for convenience.\n   */\n  public final Period getPeriod(int periodIndex, Period period) {\n    return getPeriod(periodIndex, period, false);\n  }\n\n  /**\n   * Populates a {@link Period} with data for the period at the specified index.\n   *\n   * @param periodIndex The index of the period.\n   * @param period The {@link Period} to populate. Must not be null.\n   * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,\n   *     the fields will be set to null. The caller should pass false for efficiency reasons unless\n   *     the fields are required.\n   * @return The populated {@link Period}, for convenience.\n   */\n  public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);\n\n  /**\n   * Returns the index of the period identified by its unique {@link Period#uid}, or {@link\n   * C#INDEX_UNSET} if the period is not in the timeline.\n   *\n   * @param uid A unique identifier for a period.\n   * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.\n   */\n  public abstract int getIndexOfPeriod(Object uid);\n\n  /**\n   * Returns the unique id of the period identified by its index in the timeline.\n   *\n   * @param periodIndex The index of the period.\n   * @return The unique id of the period.\n   */\n  public abstract Object getUidOfPeriod(int periodIndex);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/WakeLockManager.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.PowerManager;\nimport android.os.PowerManager.WakeLock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * Handles a {@link WakeLock}.\n *\n * <p>The handling of wake locks requires the {@link android.Manifest.permission#WAKE_LOCK}\n * permission.\n */\n/* package */ final class WakeLockManager {\n\n  private static final String TAG = \"WakeLockManager\";\n  private static final String WAKE_LOCK_TAG = \"ExoPlayer:WakeLockManager\";\n\n  @Nullable private final PowerManager powerManager;\n  @Nullable private WakeLock wakeLock;\n  private boolean enabled;\n  private boolean stayAwake;\n\n  public WakeLockManager(Context context) {\n    powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);\n  }\n\n  /**\n   * Sets whether to enable the acquiring and releasing of the {@link WakeLock}.\n   *\n   * <p>By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if\n   * necessary. Disabling this will release the wake lock if it is held.\n   *\n   * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please\n   *     note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK}\n   *     permission.\n   */\n  public void setEnabled(boolean enabled) {\n    if (enabled) {\n      if (wakeLock == null) {\n        if (powerManager == null) {\n          Log.w(TAG, \"PowerManager was null, therefore the WakeLock was not created.\");\n          return;\n        }\n        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);\n      }\n    }\n\n    this.enabled = enabled;\n    updateWakeLock();\n  }\n\n  /**\n   * Sets whether to acquire or release the {@link WakeLock}.\n   *\n   * <p>Please note this method requires wake lock handling to be enabled through setEnabled(boolean\n   * enable) to actually have an impact on the {@link WakeLock}.\n   *\n   * @param stayAwake True if the player should acquire the {@link WakeLock}. False if the player\n   *     should release.\n   */\n  public void setStayAwake(boolean stayAwake) {\n    this.stayAwake = stayAwake;\n    updateWakeLock();\n  }\n\n  // WakelockTimeout suppressed because the time the wake lock is needed for is unknown (could be\n  // listening to radio with screen off for multiple hours), therefore we can not determine a\n  // reasonable timeout that would not affect the user.\n  @SuppressLint(\"WakelockTimeout\")\n  private void updateWakeLock() {\n    // Needed for the library nullness check. If enabled is true, the wakelock will not be null.\n    if (wakeLock != null) {\n      if (enabled) {\n        if (stayAwake && !wakeLock.isHeld()) {\n          wakeLock.acquire();\n        } else if (!stayAwake && wakeLock.isHeld()) {\n          wakeLock.release();\n        }\n      } else if (wakeLock.isHeld()) {\n        wakeLock.release();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport android.view.Surface;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.PlaybackSuppressionReason;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.Timeline.Window;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.audio.AudioListener;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.video.VideoListener;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by\n * listening to all available ExoPlayer listeners.\n */\npublic class AnalyticsCollector\n    implements Player.EventListener,\n        MetadataOutput,\n        AudioRendererEventListener,\n        VideoRendererEventListener,\n        MediaSourceEventListener,\n        BandwidthMeter.EventListener,\n        DefaultDrmSessionEventListener,\n        VideoListener,\n        AudioListener {\n\n  private final CopyOnWriteArraySet<AnalyticsListener> listeners;\n  private final Clock clock;\n  private final Window window;\n  private final MediaPeriodQueueTracker mediaPeriodQueueTracker;\n\n  private @MonotonicNonNull Player player;\n\n  /**\n   * Creates an analytics collector.\n   *\n   * @param clock A {@link Clock} used to generate timestamps.\n   */\n  public AnalyticsCollector(Clock clock) {\n    this.clock = Assertions.checkNotNull(clock);\n    listeners = new CopyOnWriteArraySet<>();\n    mediaPeriodQueueTracker = new MediaPeriodQueueTracker();\n    window = new Window();\n  }\n\n  /**\n   * Adds a listener for analytics events.\n   *\n   * @param listener The listener to add.\n   */\n  public void addListener(AnalyticsListener listener) {\n    listeners.add(listener);\n  }\n\n  /**\n   * Removes a previously added analytics event listener.\n   *\n   * @param listener The listener to remove.\n   */\n  public void removeListener(AnalyticsListener listener) {\n    listeners.remove(listener);\n  }\n\n  /**\n   * Sets the player for which data will be collected. Must only be called if no player has been set\n   * yet or the current player is idle.\n   *\n   * @param player The {@link Player} for which data will be collected.\n   */\n  public void setPlayer(Player player) {\n    Assertions.checkState(\n        this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());\n    this.player = Assertions.checkNotNull(player);\n  }\n\n  // External events.\n\n  /**\n   * Notify analytics collector that a seek operation will start. Should be called before the player\n   * adjusts its state and position to the seek.\n   */\n  public final void notifySeekStarted() {\n    if (!mediaPeriodQueueTracker.isSeeking()) {\n      EventTime eventTime = generatePlayingMediaPeriodEventTime();\n      mediaPeriodQueueTracker.onSeekStarted();\n      for (AnalyticsListener listener : listeners) {\n        listener.onSeekStarted(eventTime);\n      }\n    }\n  }\n\n  /**\n   * Resets the analytics collector for a new media source. Should be called before the player is\n   * prepared with a new media source.\n   */\n  public final void resetForNewMediaSource() {\n    // Copying the list is needed because onMediaPeriodReleased will modify the list.\n    List<MediaPeriodInfo> mediaPeriodInfos =\n        new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);\n    for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) {\n      onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);\n    }\n  }\n\n  // MetadataOutput implementation.\n\n  @Override\n  public final void onMetadata(Metadata metadata) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onMetadata(eventTime, metadata);\n    }\n  }\n\n  // AudioRendererEventListener implementation.\n\n  @Override\n  public final void onAudioEnabled(DecoderCounters counters) {\n    // The renderers are only enabled after we changed the playing media period.\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters);\n    }\n  }\n\n  @Override\n  public final void onAudioDecoderInitialized(\n      String decoderName, long initializedTimestampMs, long initializationDurationMs) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderInitialized(\n          eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);\n    }\n  }\n\n  @Override\n  public final void onAudioInputFormatChanged(Format format) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);\n    }\n  }\n\n  @Override\n  public final void onAudioSinkUnderrun(\n      int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n    }\n  }\n\n  @Override\n  public final void onAudioDisabled(DecoderCounters counters) {\n    // The renderers are disabled after we changed the playing media period on the playback thread\n    // but before this change is reported to the app thread.\n    EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);\n    }\n  }\n\n  // AudioListener implementation.\n\n  @Override\n  public final void onAudioSessionId(int audioSessionId) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onAudioSessionId(eventTime, audioSessionId);\n    }\n  }\n\n  @Override\n  public void onAudioAttributesChanged(AudioAttributes audioAttributes) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onAudioAttributesChanged(eventTime, audioAttributes);\n    }\n  }\n\n  @Override\n  public void onVolumeChanged(float audioVolume) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onVolumeChanged(eventTime, audioVolume);\n    }\n  }\n\n  // VideoRendererEventListener implementation.\n\n  @Override\n  public final void onVideoEnabled(DecoderCounters counters) {\n    // The renderers are only enabled after we changed the playing media period.\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters);\n    }\n  }\n\n  @Override\n  public final void onVideoDecoderInitialized(\n      String decoderName, long initializedTimestampMs, long initializationDurationMs) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderInitialized(\n          eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);\n    }\n  }\n\n  @Override\n  public final void onVideoInputFormatChanged(Format format) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);\n    }\n  }\n\n  @Override\n  public final void onDroppedFrames(int count, long elapsedMs) {\n    EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDroppedVideoFrames(eventTime, count, elapsedMs);\n    }\n  }\n\n  @Override\n  public final void onVideoDisabled(DecoderCounters counters) {\n    // The renderers are disabled after we changed the playing media period on the playback thread\n    // but before this change is reported to the app thread.\n    EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);\n    }\n  }\n\n  @Override\n  public final void onRenderedFirstFrame(@Nullable Surface surface) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onRenderedFirstFrame(eventTime, surface);\n    }\n  }\n\n  // VideoListener implementation.\n\n  @Override\n  public final void onRenderedFirstFrame() {\n    // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame.\n  }\n\n  @Override\n  public final void onVideoSizeChanged(\n      int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onVideoSizeChanged(\n          eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n    }\n  }\n\n  @Override\n  public void onSurfaceSizeChanged(int width, int height) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onSurfaceSizeChanged(eventTime, width, height);\n    }\n  }\n\n  // MediaSourceEventListener implementation.\n\n  @Override\n  public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n    mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onMediaPeriodCreated(eventTime);\n    }\n  }\n\n  @Override\n  public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) {\n      for (AnalyticsListener listener : listeners) {\n        listener.onMediaPeriodReleased(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public final void onLoadStarted(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);\n    }\n  }\n\n  @Override\n  public final void onLoadCompleted(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);\n    }\n  }\n\n  @Override\n  public final void onLoadCanceled(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);\n    }\n  }\n\n  @Override\n  public final void onLoadError(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData,\n      IOException error,\n      boolean wasCanceled) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);\n    }\n  }\n\n  @Override\n  public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {\n    mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId);\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onReadingStarted(eventTime);\n    }\n  }\n\n  @Override\n  public final void onUpstreamDiscarded(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onUpstreamDiscarded(eventTime, mediaLoadData);\n    }\n  }\n\n  @Override\n  public final void onDownstreamFormatChanged(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);\n    for (AnalyticsListener listener : listeners) {\n      listener.onDownstreamFormatChanged(eventTime, mediaLoadData);\n    }\n  }\n\n  // Player.EventListener implementation.\n\n  // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous\n  // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of\n  // having slightly different real times.\n\n  @Override\n  public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {\n    mediaPeriodQueueTracker.onTimelineChanged(timeline);\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onTimelineChanged(eventTime, reason);\n    }\n  }\n\n  @Override\n  public final void onTracksChanged(\n      TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onTracksChanged(eventTime, trackGroups, trackSelections);\n    }\n  }\n\n  @Override\n  public final void onLoadingChanged(boolean isLoading) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onLoadingChanged(eventTime, isLoading);\n    }\n  }\n\n  @Override\n  public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);\n    }\n  }\n\n  @Override\n  public void onPlaybackSuppressionReasonChanged(\n      @PlaybackSuppressionReason int playbackSuppressionReason) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason);\n    }\n  }\n\n  @Override\n  public void onIsPlayingChanged(boolean isPlaying) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onIsPlayingChanged(eventTime, isPlaying);\n    }\n  }\n\n  @Override\n  public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onRepeatModeChanged(eventTime, repeatMode);\n    }\n  }\n\n  @Override\n  public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);\n    }\n  }\n\n  @Override\n  public final void onPlayerError(ExoPlaybackException error) {\n    EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onPlayerError(eventTime, error);\n    }\n  }\n\n  @Override\n  public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n    mediaPeriodQueueTracker.onPositionDiscontinuity(reason);\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onPositionDiscontinuity(eventTime, reason);\n    }\n  }\n\n  @Override\n  public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n    EventTime eventTime = generatePlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onPlaybackParametersChanged(eventTime, playbackParameters);\n    }\n  }\n\n  @Override\n  public final void onSeekProcessed() {\n    if (mediaPeriodQueueTracker.isSeeking()) {\n      mediaPeriodQueueTracker.onSeekProcessed();\n      EventTime eventTime = generatePlayingMediaPeriodEventTime();\n      for (AnalyticsListener listener : listeners) {\n        listener.onSeekProcessed(eventTime);\n      }\n    }\n  }\n\n  // BandwidthMeter.Listener implementation.\n\n  @Override\n  public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {\n    EventTime eventTime = generateLoadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate);\n    }\n  }\n\n  // DefaultDrmSessionManager.EventListener implementation.\n\n  @Override\n  public final void onDrmSessionAcquired() {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmSessionAcquired(eventTime);\n    }\n  }\n\n  @Override\n  public final void onDrmKeysLoaded() {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmKeysLoaded(eventTime);\n    }\n  }\n\n  @Override\n  public final void onDrmSessionManagerError(Exception error) {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmSessionManagerError(eventTime, error);\n    }\n  }\n\n  @Override\n  public final void onDrmKeysRestored() {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmKeysRestored(eventTime);\n    }\n  }\n\n  @Override\n  public final void onDrmKeysRemoved() {\n    EventTime eventTime = generateReadingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmKeysRemoved(eventTime);\n    }\n  }\n\n  @Override\n  public final void onDrmSessionReleased() {\n    EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();\n    for (AnalyticsListener listener : listeners) {\n      listener.onDrmSessionReleased(eventTime);\n    }\n  }\n\n  // Internal methods.\n\n  /** Returns read-only set of registered listeners. */\n  protected Set<AnalyticsListener> getListeners() {\n    return Collections.unmodifiableSet(listeners);\n  }\n\n  /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */\n  @RequiresNonNull(\"player\")\n  protected EventTime generateEventTime(\n      Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {\n    if (timeline.isEmpty()) {\n      // Ensure media period id is only reported together with a valid timeline.\n      mediaPeriodId = null;\n    }\n    long realtimeMs = clock.elapsedRealtime();\n    long eventPositionMs;\n    boolean isInCurrentWindow =\n        timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex();\n    if (mediaPeriodId != null && mediaPeriodId.isAd()) {\n      boolean isCurrentAd =\n          isInCurrentWindow\n              && player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex\n              && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup;\n      // Assume start position of 0 for future ads.\n      eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0;\n    } else if (isInCurrentWindow) {\n      eventPositionMs = player.getContentPosition();\n    } else {\n      // Assume default start position for future content windows. If timeline is not available yet,\n      // assume start position of 0.\n      eventPositionMs =\n          timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs();\n    }\n    return new EventTime(\n        realtimeMs,\n        timeline,\n        windowIndex,\n        mediaPeriodId,\n        eventPositionMs,\n        player.getCurrentPosition(),\n        player.getTotalBufferedDuration());\n  }\n\n  private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) {\n    Assertions.checkNotNull(player);\n    if (mediaPeriodInfo == null) {\n      int windowIndex = player.getCurrentWindowIndex();\n      mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);\n      if (mediaPeriodInfo == null) {\n        Timeline timeline = player.getCurrentTimeline();\n        boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();\n        return generateEventTime(\n            windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);\n      }\n    }\n    return generateEventTime(\n        mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);\n  }\n\n  private EventTime generateLastReportedPlayingMediaPeriodEventTime() {\n    return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod());\n  }\n\n  private EventTime generatePlayingMediaPeriodEventTime() {\n    return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod());\n  }\n\n  private EventTime generateReadingMediaPeriodEventTime() {\n    return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod());\n  }\n\n  private EventTime generateLoadingMediaPeriodEventTime() {\n    return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod());\n  }\n\n  private EventTime generateMediaPeriodEventTime(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {\n    Assertions.checkNotNull(player);\n    if (mediaPeriodId != null) {\n      MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId);\n      return mediaPeriodInfo != null\n          ? generateEventTime(mediaPeriodInfo)\n          : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);\n    }\n    Timeline timeline = player.getCurrentTimeline();\n    boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();\n    return generateEventTime(\n        windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);\n  }\n\n  /** Keeps track of the active media periods and currently playing and reading media period. */\n  private static final class MediaPeriodQueueTracker {\n\n    // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue\n    // changes, which would hopefully remove the need to track the queue here.\n\n    private final ArrayList<MediaPeriodInfo> mediaPeriodInfoQueue;\n    private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;\n    private final Period period;\n\n    @Nullable private MediaPeriodInfo lastPlayingMediaPeriod;\n    @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod;\n    @Nullable private MediaPeriodInfo readingMediaPeriod;\n    private Timeline timeline;\n    private boolean isSeeking;\n\n    public MediaPeriodQueueTracker() {\n      mediaPeriodInfoQueue = new ArrayList<>();\n      mediaPeriodIdToInfo = new HashMap<>();\n      period = new Period();\n      timeline = Timeline.EMPTY;\n    }\n\n    /**\n     * Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is\n     * the playing media period unless the player hasn't started playing yet (in which case it is\n     * the loading media period or null). While the player is seeking or preparing, this method will\n     * always return null to reflect the uncertainty about the current playing period. May also be\n     * null, if the timeline is empty or no media period is active yet.\n     */\n    @Nullable\n    public MediaPeriodInfo getPlayingMediaPeriod() {\n      return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking\n          ? null\n          : mediaPeriodInfoQueue.get(0);\n    }\n\n    /**\n     * Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the\n     * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()}\n     * unless the player is currently seeking or being prepared in which case the previous period is\n     * reported until the seek or preparation is processed. May be null, if no media period is\n     * active yet.\n     */\n    @Nullable\n    public MediaPeriodInfo getLastReportedPlayingMediaPeriod() {\n      return lastReportedPlayingMediaPeriod;\n    }\n\n    /**\n     * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player.\n     * May be null, if the player is not reading a media period.\n     */\n    @Nullable\n    public MediaPeriodInfo getReadingMediaPeriod() {\n      return readingMediaPeriod;\n    }\n\n    /**\n     * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is\n     * currently loading or will be the next one loading. May be null, if no media period is active\n     * yet.\n     */\n    @Nullable\n    public MediaPeriodInfo getLoadingMediaPeriod() {\n      return mediaPeriodInfoQueue.isEmpty()\n          ? null\n          : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1);\n    }\n\n    /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */\n    @Nullable\n    public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) {\n      return mediaPeriodIdToInfo.get(mediaPeriodId);\n    }\n\n    /** Returns whether the player is currently seeking. */\n    public boolean isSeeking() {\n      return isSeeking;\n    }\n\n    /**\n     * Tries to find an existing media period info from the specified window index. Only returns a\n     * non-null media period info if there is a unique, unambiguous match.\n     */\n    @Nullable\n    public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) {\n      MediaPeriodInfo match = null;\n      for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {\n        MediaPeriodInfo info = mediaPeriodInfoQueue.get(i);\n        int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);\n        if (periodIndex != C.INDEX_UNSET\n            && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) {\n          if (match != null) {\n            // Ambiguous match.\n            return null;\n          }\n          match = info;\n        }\n      }\n      return match;\n    }\n\n    /** Updates the queue with a reported position discontinuity . */\n    public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n      lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;\n    }\n\n    /** Updates the queue with a reported timeline change. */\n    public void onTimelineChanged(Timeline timeline) {\n      for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {\n        MediaPeriodInfo newMediaPeriodInfo =\n            updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline);\n        mediaPeriodInfoQueue.set(i, newMediaPeriodInfo);\n        mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo);\n      }\n      if (readingMediaPeriod != null) {\n        readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);\n      }\n      this.timeline = timeline;\n      lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;\n    }\n\n    /** Updates the queue with a reported start of seek. */\n    public void onSeekStarted() {\n      isSeeking = true;\n    }\n\n    /** Updates the queue with a reported processed seek. */\n    public void onSeekProcessed() {\n      isSeeking = false;\n      lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;\n    }\n\n    /** Updates the queue with a newly created media period. */\n    public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n      int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);\n      boolean isInTimeline = periodIndex != C.INDEX_UNSET;\n      MediaPeriodInfo mediaPeriodInfo =\n          new MediaPeriodInfo(\n              mediaPeriodId,\n              isInTimeline ? timeline : Timeline.EMPTY,\n              isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);\n      mediaPeriodInfoQueue.add(mediaPeriodInfo);\n      mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);\n      lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);\n      if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) {\n        lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;\n      }\n    }\n\n    /**\n     * Updates the queue with a released media period. Returns whether the media period was still in\n     * the queue.\n     */\n    public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {\n      MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);\n      if (mediaPeriodInfo == null) {\n        // The media period has already been removed from the queue in resetForNewMediaSource().\n        return false;\n      }\n      mediaPeriodInfoQueue.remove(mediaPeriodInfo);\n      if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) {\n        readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0);\n      }\n      if (!mediaPeriodInfoQueue.isEmpty()) {\n        lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);\n      }\n      return true;\n    }\n\n    /** Update the queue with a change in the reading media period. */\n    public void onReadingStarted(MediaPeriodId mediaPeriodId) {\n      readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId);\n    }\n\n    private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline(\n        MediaPeriodInfo info, Timeline newTimeline) {\n      int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);\n      if (newPeriodIndex == C.INDEX_UNSET) {\n        // Media period is not yet or no longer available in the new timeline. Keep it as it is.\n        return info;\n      }\n      int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex;\n      return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex);\n    }\n  }\n\n  /** Information about a media period and its associated timeline. */\n  private static final class MediaPeriodInfo {\n\n    /** The {@link MediaPeriodId} of the media period. */\n    public final MediaPeriodId mediaPeriodId;\n    /**\n     * The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the\n     * media period is not part of a known timeline yet.\n     */\n    public final Timeline timeline;\n    /**\n     * The window index of the media period in the timeline. If the timeline is empty, this is the\n     * prospective window index.\n     */\n    public final int windowIndex;\n\n    public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) {\n      this.mediaPeriodId = mediaPeriodId;\n      this.timeline = timeline;\n      this.windowIndex = windowIndex;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport android.view.Surface;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Player.PlaybackSuppressionReason;\nimport com.google.android.exoplayer2.Player.TimelineChangeReason;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.audio.AudioSink;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport java.io.IOException;\n\n/**\n * A listener for analytics events.\n *\n * <p>All events are recorded with an {@link EventTime} specifying the elapsed real time and media\n * time at the time of the event.\n *\n * <p>All methods have no-op default implementations to allow selective overrides.\n */\npublic interface AnalyticsListener {\n\n  /** Time information of an event. */\n  final class EventTime {\n\n    /**\n     * Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at the time of the\n     * event, in milliseconds.\n     */\n    public final long realtimeMs;\n\n    /** Timeline at the time of the event. */\n    public final Timeline timeline;\n\n    /**\n     * Window index in the {@link #timeline} this event belongs to, or the prospective window index\n     * if the timeline is not yet known and empty.\n     */\n    public final int windowIndex;\n\n    /**\n     * Media period identifier for the media period this event belongs to, or {@code null} if the\n     * event is not associated with a specific media period.\n     */\n    @Nullable public final MediaPeriodId mediaPeriodId;\n\n    /**\n     * Position in the window or ad this event belongs to at the time of the event, in milliseconds.\n     */\n    public final long eventPlaybackPositionMs;\n\n    /**\n     * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the\n     * currently playing ad at the time of the event, in milliseconds.\n     */\n    public final long currentPlaybackPositionMs;\n\n    /**\n     * Total buffered duration from {@link #currentPlaybackPositionMs} at the time of the event, in\n     * milliseconds. This includes pre-buffered data for subsequent ads and windows.\n     */\n    public final long totalBufferedDurationMs;\n\n    /**\n     * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at\n     *     the time of the event, in milliseconds.\n     * @param timeline Timeline at the time of the event.\n     * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the\n     *     prospective window index if the timeline is not yet known and empty.\n     * @param mediaPeriodId Media period identifier for the media period this event belongs to, or\n     *     {@code null} if the event is not associated with a specific media period.\n     * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time\n     *     of the event, in milliseconds.\n     * @param currentPlaybackPositionMs Position in the current timeline window ({@link\n     *     Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in\n     *     milliseconds.\n     * @param totalBufferedDurationMs Total buffered duration from {@link\n     *     #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes\n     *     pre-buffered data for subsequent ads and windows.\n     */\n    public EventTime(\n        long realtimeMs,\n        Timeline timeline,\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        long eventPlaybackPositionMs,\n        long currentPlaybackPositionMs,\n        long totalBufferedDurationMs) {\n      this.realtimeMs = realtimeMs;\n      this.timeline = timeline;\n      this.windowIndex = windowIndex;\n      this.mediaPeriodId = mediaPeriodId;\n      this.eventPlaybackPositionMs = eventPlaybackPositionMs;\n      this.currentPlaybackPositionMs = currentPlaybackPositionMs;\n      this.totalBufferedDurationMs = totalBufferedDurationMs;\n    }\n  }\n\n  /**\n   * Called when the player state changed.\n   *\n   * @param eventTime The event time.\n   * @param playWhenReady Whether the playback will proceed when ready.\n   * @param playbackState The new {@link Player.State playback state}.\n   */\n  default void onPlayerStateChanged(\n          EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {}\n\n  /**\n   * Called when playback suppression reason changed.\n   *\n   * @param eventTime The event time.\n   * @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}.\n   */\n  default void onPlaybackSuppressionReasonChanged(\n          EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {}\n\n  /**\n   * Called when the player starts or stops playing.\n   *\n   * @param eventTime The event time.\n   * @param isPlaying Whether the player is playing.\n   */\n  default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {}\n\n  /**\n   * Called when the timeline changed.\n   *\n   * @param eventTime The event time.\n   * @param reason The reason for the timeline change.\n   */\n  default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {}\n\n  /**\n   * Called when a position discontinuity occurred.\n   *\n   * @param eventTime The event time.\n   * @param reason The reason for the position discontinuity.\n   */\n  default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}\n\n  /**\n   * Called when a seek operation started.\n   *\n   * @param eventTime The event time.\n   */\n  default void onSeekStarted(EventTime eventTime) {}\n\n  /**\n   * Called when a seek operation was processed.\n   *\n   * @param eventTime The event time.\n   */\n  default void onSeekProcessed(EventTime eventTime) {}\n\n  /**\n   * Called when the playback parameters changed.\n   *\n   * @param eventTime The event time.\n   * @param playbackParameters The new playback parameters.\n   */\n  default void onPlaybackParametersChanged(\n          EventTime eventTime, PlaybackParameters playbackParameters) {}\n\n  /**\n   * Called when the repeat mode changed.\n   *\n   * @param eventTime The event time.\n   * @param repeatMode The new repeat mode.\n   */\n  default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {}\n\n  /**\n   * Called when the shuffle mode changed.\n   *\n   * @param eventTime The event time.\n   * @param shuffleModeEnabled Whether the shuffle mode is enabled.\n   */\n  default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}\n\n  /**\n   * Called when the player starts or stops loading data from a source.\n   *\n   * @param eventTime The event time.\n   * @param isLoading Whether the player is loading.\n   */\n  default void onLoadingChanged(EventTime eventTime, boolean isLoading) {}\n\n  /**\n   * Called when a fatal player error occurred.\n   *\n   * @param eventTime The event time.\n   * @param error The error.\n   */\n  default void onPlayerError(EventTime eventTime, ExoPlaybackException error) {}\n\n  /**\n   * Called when the available or selected tracks for the renderers changed.\n   *\n   * @param eventTime The event time.\n   * @param trackGroups The available tracks. May be empty.\n   * @param trackSelections The track selections for each renderer. May contain null elements.\n   */\n  default void onTracksChanged(\n          EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}\n\n  /**\n   * Called when a media source started loading data.\n   *\n   * @param eventTime The event time.\n   * @param loadEventInfo The {@link LoadEventInfo} defining the load event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadStarted(\n          EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a media source completed loading data.\n   *\n   * @param eventTime The event time.\n   * @param loadEventInfo The {@link LoadEventInfo} defining the load event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadCompleted(\n          EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a media source canceled loading data.\n   *\n   * @param eventTime The event time.\n   * @param loadEventInfo The {@link LoadEventInfo} defining the load event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadCanceled(\n          EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a media source loading error occurred. These errors are just for informational\n   * purposes and the player may recover.\n   *\n   * @param eventTime The event time.\n   * @param loadEventInfo The {@link LoadEventInfo} defining the load event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   * @param error The load error.\n   * @param wasCanceled Whether the load was canceled as a result of the error.\n   */\n  default void onLoadError(\n          EventTime eventTime,\n          LoadEventInfo loadEventInfo,\n          MediaLoadData mediaLoadData,\n          IOException error,\n          boolean wasCanceled) {}\n\n  /**\n   * Called when the downstream format sent to the renderers changed.\n   *\n   * @param eventTime The event time.\n   * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data.\n   */\n  default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when data is removed from the back of a media buffer, typically so that it can be\n   * re-buffered in a different format.\n   *\n   * @param eventTime The event time.\n   * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.\n   */\n  default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a media source created a media period.\n   *\n   * @param eventTime The event time.\n   */\n  default void onMediaPeriodCreated(EventTime eventTime) {}\n\n  /**\n   * Called when a media source released a media period.\n   *\n   * @param eventTime The event time.\n   */\n  default void onMediaPeriodReleased(EventTime eventTime) {}\n\n  /**\n   * Called when the player started reading a media period.\n   *\n   * @param eventTime The event time.\n   */\n  default void onReadingStarted(EventTime eventTime) {}\n\n  /**\n   * Called when the bandwidth estimate for the current data source has been updated.\n   *\n   * @param eventTime The event time.\n   * @param totalLoadTimeMs The total time spend loading this update is based on, in milliseconds.\n   * @param totalBytesLoaded The total bytes loaded this update is based on.\n   * @param bitrateEstimate The bandwidth estimate, in bits per second.\n   */\n  default void onBandwidthEstimate(\n          EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}\n\n  /**\n   * Called when the output surface size changed.\n   *\n   * @param eventTime The event time.\n   * @param width The surface width in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if the\n   *     video is not rendered onto a surface.\n   * @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if\n   *     the video is not rendered onto a surface.\n   */\n  default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}\n\n  /**\n   * Called when there is {@link Metadata} associated with the current playback time.\n   *\n   * @param eventTime The event time.\n   * @param metadata The metadata.\n   */\n  default void onMetadata(EventTime eventTime, Metadata metadata) {}\n\n  /**\n   * Called when an audio or video decoder has been enabled.\n   *\n   * @param eventTime The event time.\n   * @param trackType The track type of the enabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or\n   *     {@link C#TRACK_TYPE_VIDEO}.\n   * @param decoderCounters The accumulated event counters associated with this decoder.\n   */\n  default void onDecoderEnabled(\n          EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}\n\n  /**\n   * Called when an audio or video decoder has been initialized.\n   *\n   * @param eventTime The event time.\n   * @param trackType The track type of the initialized decoder. Either {@link C#TRACK_TYPE_AUDIO}\n   *     or {@link C#TRACK_TYPE_VIDEO}.\n   * @param decoderName The decoder that was created.\n   * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds.\n   */\n  default void onDecoderInitialized(\n          EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}\n\n  /**\n   * Called when an audio or video decoder input format changed.\n   *\n   * @param eventTime The event time.\n   * @param trackType The track type of the decoder whose format changed. Either {@link\n   *     C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}.\n   * @param format The new input format for the decoder.\n   */\n  default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}\n\n  /**\n   * Called when an audio or video decoder has been disabled.\n   *\n   * @param eventTime The event time.\n   * @param trackType The track type of the disabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or\n   *     {@link C#TRACK_TYPE_VIDEO}.\n   * @param decoderCounters The accumulated event counters associated with this decoder.\n   */\n  default void onDecoderDisabled(\n          EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}\n\n  /**\n   * Called when the audio session id is set.\n   *\n   * @param eventTime The event time.\n   * @param audioSessionId The audio session id.\n   */\n  default void onAudioSessionId(EventTime eventTime, int audioSessionId) {}\n\n  /**\n   * Called when the audio attributes change.\n   *\n   * @param eventTime The event time.\n   * @param audioAttributes The audio attributes.\n   */\n  default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {}\n\n  /**\n   * Called when the volume changes.\n   *\n   * @param eventTime The event time.\n   * @param volume The new volume, with 0 being silence and 1 being unity gain.\n   */\n  default void onVolumeChanged(EventTime eventTime, float volume) {}\n\n  /**\n   * Called when an audio underrun occurred.\n   *\n   * @param eventTime The event time.\n   * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes.\n   * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is\n   *     configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,\n   *     as the buffered media can have a variable bitrate so the duration may be unknown.\n   * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.\n   */\n  default void onAudioUnderrun(\n          EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}\n\n  /**\n   * Called after video frames have been dropped.\n   *\n   * @param eventTime The event time.\n   * @param droppedFrames The number of dropped frames since the last call to this method.\n   * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration\n   *     is timed from when the renderer was started or from when dropped frames were last reported\n   *     (whichever was more recent), and not from when the first of the reported drops occurred.\n   */\n  default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}\n\n  /**\n   * Called before a frame is rendered for the first time since setting the surface, and each time\n   * there's a change in the size or pixel aspect ratio of the video being rendered.\n   *\n   * @param eventTime The event time.\n   * @param width The width of the video.\n   * @param height The height of the video.\n   * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise\n   *     rotation in degrees that the application should apply for the video for it to be rendered\n   *     in the correct orientation. This value will always be zero on API levels 21 and above,\n   *     since the renderer will apply all necessary rotations internally.\n   * @param pixelWidthHeightRatio The width to height ratio of each pixel.\n   */\n  default void onVideoSizeChanged(\n          EventTime eventTime,\n          int width,\n          int height,\n          int unappliedRotationDegrees,\n          float pixelWidthHeightRatio) {}\n\n  /**\n   * Called when a frame is rendered for the first time since setting the surface, and when a frame\n   * is rendered for the first time since the renderer was reset.\n   *\n   * @param eventTime The event time.\n   * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if\n   *     the renderer renders to something that isn't a {@link Surface}.\n   */\n  default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {}\n\n  /**\n   * Called each time a drm session is acquired.\n   *\n   * @param eventTime The event time.\n   */\n  default void onDrmSessionAcquired(EventTime eventTime) {}\n\n  /**\n   * Called each time drm keys are loaded.\n   *\n   * @param eventTime The event time.\n   */\n  default void onDrmKeysLoaded(EventTime eventTime) {}\n\n  /**\n   * Called when a drm error occurs. These errors are just for informational purposes and the player\n   * may recover.\n   *\n   * @param eventTime The event time.\n   * @param error The error.\n   */\n  default void onDrmSessionManagerError(EventTime eventTime, Exception error) {}\n\n  /**\n   * Called each time offline drm keys are restored.\n   *\n   * @param eventTime The event time.\n   */\n  default void onDrmKeysRestored(EventTime eventTime) {}\n\n  /**\n   * Called each time offline drm keys are removed.\n   *\n   * @param eventTime The event time.\n   */\n  default void onDrmKeysRemoved(EventTime eventTime) {}\n\n  /**\n   * Called each time a drm session is released.\n   *\n   * @param eventTime The event time.\n   */\n  default void onDrmSessionReleased(EventTime eventTime) {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\n/**\n * @deprecated Use {@link AnalyticsListener} directly for selective overrides as all methods are\n *     implemented as no-op default methods.\n */\n@Deprecated\npublic abstract class DefaultAnalyticsListener implements AnalyticsListener {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport android.util.Base64;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Random;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the\n * timeline and also for each ad within the windows.\n *\n * <p>Sessions are identified by Base64-encoded, URL-safe, random strings.\n */\npublic final class DefaultPlaybackSessionManager implements PlaybackSessionManager {\n\n  private static final Random RANDOM = new Random();\n  private static final int SESSION_ID_LENGTH = 12;\n\n  private final Timeline.Window window;\n  private final Timeline.Period period;\n  private final HashMap<String, SessionDescriptor> sessions;\n\n  private @MonotonicNonNull Listener listener;\n  private Timeline currentTimeline;\n  @Nullable private MediaPeriodId currentMediaPeriodId;\n  @Nullable private String activeSessionId;\n\n  /** Creates session manager. */\n  public DefaultPlaybackSessionManager() {\n    window = new Timeline.Window();\n    period = new Timeline.Period();\n    sessions = new HashMap<>();\n    currentTimeline = Timeline.EMPTY;\n  }\n\n  @Override\n  public void setListener(Listener listener) {\n    this.listener = listener;\n  }\n\n  @Override\n  public synchronized String getSessionForMediaPeriodId(\n      Timeline timeline, MediaPeriodId mediaPeriodId) {\n    int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;\n    return getOrAddSession(windowIndex, mediaPeriodId).sessionId;\n  }\n\n  @Override\n  public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) {\n    SessionDescriptor sessionDescriptor = sessions.get(sessionId);\n    if (sessionDescriptor == null) {\n      return false;\n    }\n    sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId);\n    return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId);\n  }\n\n  @Override\n  public synchronized void updateSessions(EventTime eventTime) {\n    boolean isObviouslyFinished =\n        eventTime.mediaPeriodId != null\n            && currentMediaPeriodId != null\n            && eventTime.mediaPeriodId.windowSequenceNumber\n                < currentMediaPeriodId.windowSequenceNumber;\n    if (!isObviouslyFinished) {\n      SessionDescriptor descriptor =\n          getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);\n      if (!descriptor.isCreated) {\n        descriptor.isCreated = true;\n        Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId);\n        if (activeSessionId == null) {\n          updateActiveSession(eventTime, descriptor);\n        }\n      }\n    }\n  }\n\n  @Override\n  public synchronized void handleTimelineUpdate(EventTime eventTime) {\n    Assertions.checkNotNull(listener);\n    Timeline previousTimeline = currentTimeline;\n    currentTimeline = eventTime.timeline;\n    Iterator<SessionDescriptor> iterator = sessions.values().iterator();\n    while (iterator.hasNext()) {\n      SessionDescriptor session = iterator.next();\n      if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {\n        iterator.remove();\n        if (session.isCreated) {\n          if (session.sessionId.equals(activeSessionId)) {\n            activeSessionId = null;\n          }\n          listener.onSessionFinished(\n              eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);\n        }\n      }\n    }\n    handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL);\n  }\n\n  @Override\n  public synchronized void handlePositionDiscontinuity(\n      EventTime eventTime, @DiscontinuityReason int reason) {\n    Assertions.checkNotNull(listener);\n    boolean hasAutomaticTransition =\n        reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION\n            || reason == Player.DISCONTINUITY_REASON_AD_INSERTION;\n    Iterator<SessionDescriptor> iterator = sessions.values().iterator();\n    while (iterator.hasNext()) {\n      SessionDescriptor session = iterator.next();\n      if (session.isFinishedAtEventTime(eventTime)) {\n        iterator.remove();\n        if (session.isCreated) {\n          boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId);\n          boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession;\n          if (isRemovingActiveSession) {\n            activeSessionId = null;\n          }\n          listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);\n        }\n      }\n    }\n    SessionDescriptor activeSessionDescriptor =\n        getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);\n    if (eventTime.mediaPeriodId != null\n        && eventTime.mediaPeriodId.isAd()\n        && (currentMediaPeriodId == null\n            || currentMediaPeriodId.windowSequenceNumber\n                != eventTime.mediaPeriodId.windowSequenceNumber\n            || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex\n            || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) {\n      // New ad playback started. Find corresponding content session and notify ad playback started.\n      MediaPeriodId contentMediaPeriodId =\n          new MediaPeriodId(\n              eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber);\n      SessionDescriptor contentSession =\n          getOrAddSession(eventTime.windowIndex, contentMediaPeriodId);\n      if (contentSession.isCreated && activeSessionDescriptor.isCreated) {\n        listener.onAdPlaybackStarted(\n            eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId);\n      }\n    }\n    updateActiveSession(eventTime, activeSessionDescriptor);\n  }\n\n  private SessionDescriptor getOrAddSession(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {\n    // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is\n    // null, there may be multiple matching sessions with different window sequence numbers or\n    // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for\n    // windows with ads, the content session is preferred over ad sessions.\n    SessionDescriptor bestMatch = null;\n    long bestMatchWindowSequenceNumber = Long.MAX_VALUE;\n    for (SessionDescriptor sessionDescriptor : sessions.values()) {\n      sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId);\n      if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) {\n        long windowSequenceNumber = sessionDescriptor.windowSequenceNumber;\n        if (windowSequenceNumber == C.INDEX_UNSET\n            || windowSequenceNumber < bestMatchWindowSequenceNumber) {\n          bestMatch = sessionDescriptor;\n          bestMatchWindowSequenceNumber = windowSequenceNumber;\n        } else if (windowSequenceNumber == bestMatchWindowSequenceNumber\n            && Util.castNonNull(bestMatch).adMediaPeriodId != null\n            && sessionDescriptor.adMediaPeriodId != null) {\n          bestMatch = sessionDescriptor;\n        }\n      }\n    }\n    if (bestMatch == null) {\n      String sessionId = generateSessionId();\n      bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);\n      sessions.put(sessionId, bestMatch);\n    }\n    return bestMatch;\n  }\n\n  @RequiresNonNull(\"listener\")\n  private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) {\n    currentMediaPeriodId = eventTime.mediaPeriodId;\n    if (sessionDescriptor.isCreated) {\n      activeSessionId = sessionDescriptor.sessionId;\n      if (!sessionDescriptor.isActive) {\n        sessionDescriptor.isActive = true;\n        listener.onSessionActive(eventTime, sessionDescriptor.sessionId);\n      }\n    }\n  }\n\n  private static String generateSessionId() {\n    byte[] randomBytes = new byte[SESSION_ID_LENGTH];\n    RANDOM.nextBytes(randomBytes);\n    return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);\n  }\n\n  /**\n   * Descriptor for a session.\n   *\n   * <p>The session may be described in one of three ways:\n   *\n   * <ul>\n   *   <li>A window index with unset window sequence number and a null ad media period id\n   *   <li>A content window with index and sequence number, but a null ad media period id.\n   *   <li>An ad with all values set.\n   * </ul>\n   */\n  private final class SessionDescriptor {\n\n    private final String sessionId;\n\n    private int windowIndex;\n    private long windowSequenceNumber;\n    private @MonotonicNonNull MediaPeriodId adMediaPeriodId;\n\n    private boolean isCreated;\n    private boolean isActive;\n\n    public SessionDescriptor(\n        String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {\n      this.sessionId = sessionId;\n      this.windowIndex = windowIndex;\n      this.windowSequenceNumber =\n          mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber;\n      if (mediaPeriodId != null && mediaPeriodId.isAd()) {\n        this.adMediaPeriodId = mediaPeriodId;\n      }\n    }\n\n    public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) {\n      windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex);\n      if (windowIndex == C.INDEX_UNSET) {\n        return false;\n      }\n      if (adMediaPeriodId == null) {\n        return true;\n      }\n      int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid);\n      return newPeriodIndex != C.INDEX_UNSET;\n    }\n\n    public boolean belongsToSession(\n        int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {\n      if (eventMediaPeriodId == null) {\n        // Events without concrete media period id are for all sessions of the same window.\n        return eventWindowIndex == windowIndex;\n      }\n      if (adMediaPeriodId == null) {\n        // If this is a content session, only events for content with the same window sequence\n        // number belong to this session.\n        return !eventMediaPeriodId.isAd()\n            && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber;\n      }\n      // If this is an ad session, only events for this ad belong to the session.\n      return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber\n          && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex\n          && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup;\n    }\n\n    public void maybeSetWindowSequenceNumber(\n        int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {\n      if (windowSequenceNumber == C.INDEX_UNSET\n          && eventWindowIndex == windowIndex\n          && eventMediaPeriodId != null\n          && !eventMediaPeriodId.isAd()) {\n        // Set window sequence number for this session as soon as we have one.\n        windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber;\n      }\n    }\n\n    public boolean isFinishedAtEventTime(EventTime eventTime) {\n      if (windowSequenceNumber == C.INDEX_UNSET) {\n        // Sessions with unspecified window sequence number are kept until we know more.\n        return false;\n      }\n      if (eventTime.mediaPeriodId == null) {\n        // For event times without media period id (e.g. after seek to new window), we only keep\n        // sessions of this window.\n        return windowIndex != eventTime.windowIndex;\n      }\n      if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) {\n        // All past window sequence numbers are finished.\n        return true;\n      }\n      if (adMediaPeriodId == null) {\n        // Current or future content is not finished.\n        return false;\n      }\n      int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);\n      int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid);\n      if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber\n          || eventPeriodIndex < adPeriodIndex) {\n        // Ads in future windows or periods are not finished.\n        return false;\n      }\n      if (eventPeriodIndex > adPeriodIndex) {\n        // Ads in past periods are finished.\n        return true;\n      }\n      if (eventTime.mediaPeriodId.isAd()) {\n        int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex;\n        int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup;\n        // Finished if event is for an ad after this one in the same period.\n        return eventAdGroup > adMediaPeriodId.adGroupIndex\n            || (eventAdGroup == adMediaPeriodId.adGroupIndex\n                && eventAdIndex > adMediaPeriodId.adIndexInAdGroup);\n      } else {\n        // Finished if the event is for content after this ad.\n        return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET\n            || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex;\n      }\n    }\n\n    private int resolveWindowIndexToNewTimeline(\n        Timeline oldTimeline, Timeline newTimeline, int windowIndex) {\n      if (windowIndex >= oldTimeline.getWindowCount()) {\n        return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET;\n      }\n      oldTimeline.getWindow(windowIndex, window);\n      for (int periodIndex = window.firstPeriodIndex;\n          periodIndex <= window.lastPeriodIndex;\n          periodIndex++) {\n        Object periodUid = oldTimeline.getUidOfPeriod(periodIndex);\n        int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid);\n        if (newPeriodIndex != C.INDEX_UNSET) {\n          return newTimeline.getPeriod(newPeriodIndex, period).windowIndex;\n        }\n      }\n      return C.INDEX_UNSET;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\n\n/**\n * Manager for active playback sessions.\n *\n * <p>The manager keeps track of the association between window index and/or media period id to\n * session identifier.\n */\npublic interface PlaybackSessionManager {\n\n  /** A listener for session updates. */\n  interface Listener {\n\n    /**\n     * Called when a new session is created as a result of {@link #updateSessions(EventTime)}.\n     *\n     * @param eventTime The {@link EventTime} at which the session is created.\n     * @param sessionId The identifier of the new session.\n     */\n    void onSessionCreated(EventTime eventTime, String sessionId);\n\n    /**\n     * Called when a session becomes active, i.e. playing in the foreground.\n     *\n     * @param eventTime The {@link EventTime} at which the session becomes active.\n     * @param sessionId The identifier of the session.\n     */\n    void onSessionActive(EventTime eventTime, String sessionId);\n\n    /**\n     * Called when a session is interrupted by ad playback.\n     *\n     * @param eventTime The {@link EventTime} at which the ad playback starts.\n     * @param contentSessionId The session identifier of the content session.\n     * @param adSessionId The identifier of the ad session.\n     */\n    void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId);\n\n    /**\n     * Called when a session is permanently finished.\n     *\n     * @param eventTime The {@link EventTime} at which the session finished.\n     * @param sessionId The identifier of the finished session.\n     * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic\n     *     transition to the next playback item.\n     */\n    void onSessionFinished(\n            EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback);\n  }\n\n  /**\n   * Sets the listener to be notified of session updates. Must be called before the session manager\n   * is used.\n   *\n   * @param listener The {@link Listener} to be notified of session updates.\n   */\n  void setListener(Listener listener);\n\n  /**\n   * Returns the session identifier for the given media period id.\n   *\n   * <p>Note that this will reserve a new session identifier if it doesn't exist yet, but will not\n   * call any {@link Listener} callbacks.\n   *\n   * @param timeline The timeline, {@code mediaPeriodId} is part of.\n   * @param mediaPeriodId A {@link MediaPeriodId}.\n   */\n  String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId);\n\n  /**\n   * Returns whether an event time belong to a session.\n   *\n   * @param eventTime The {@link EventTime}.\n   * @param sessionId A session identifier.\n   * @return Whether the event belongs to the specified session.\n   */\n  boolean belongsToSession(EventTime eventTime, String sessionId);\n\n  /**\n   * Updates or creates sessions based on a player {@link EventTime}.\n   *\n   * @param eventTime The {@link EventTime}.\n   */\n  void updateSessions(EventTime eventTime);\n\n  /**\n   * Updates the session associations to a new timeline.\n   *\n   * @param eventTime The event time with the timeline change.\n   */\n  void handleTimelineUpdate(EventTime eventTime);\n\n  /**\n   * Handles a position discontinuity.\n   *\n   * @param eventTime The event time of the position discontinuity.\n   * @param reason The {@link DiscontinuityReason}.\n   */\n  void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport android.os.SystemClock;\nimport android.util.Pair;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.Collections;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** Statistics about playbacks. */\npublic final class PlaybackStats {\n\n  /**\n   * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link\n   * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link\n   * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING},\n   * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link\n   * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link\n   * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link\n   * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link\n   * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})\n  @IntDef({\n    PLAYBACK_STATE_NOT_STARTED,\n    PLAYBACK_STATE_JOINING_BACKGROUND,\n    PLAYBACK_STATE_JOINING_FOREGROUND,\n    PLAYBACK_STATE_PLAYING,\n    PLAYBACK_STATE_PAUSED,\n    PLAYBACK_STATE_SEEKING,\n    PLAYBACK_STATE_BUFFERING,\n    PLAYBACK_STATE_PAUSED_BUFFERING,\n    PLAYBACK_STATE_SEEK_BUFFERING,\n    PLAYBACK_STATE_SUPPRESSED,\n    PLAYBACK_STATE_SUPPRESSED_BUFFERING,\n    PLAYBACK_STATE_ENDED,\n    PLAYBACK_STATE_STOPPED,\n    PLAYBACK_STATE_FAILED,\n    PLAYBACK_STATE_INTERRUPTED_BY_AD,\n    PLAYBACK_STATE_ABANDONED\n  })\n  @interface PlaybackState {}\n  /** Playback has not started (initial state). */\n  public static final int PLAYBACK_STATE_NOT_STARTED = 0;\n  /** Playback is buffering in the background for initial playback start. */\n  public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1;\n  /** Playback is buffering in the foreground for initial playback start. */\n  public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2;\n  /** Playback is actively playing. */\n  public static final int PLAYBACK_STATE_PLAYING = 3;\n  /** Playback is paused but ready to play. */\n  public static final int PLAYBACK_STATE_PAUSED = 4;\n  /** Playback is handling a seek. */\n  public static final int PLAYBACK_STATE_SEEKING = 5;\n  /** Playback is buffering to resume active playback. */\n  public static final int PLAYBACK_STATE_BUFFERING = 6;\n  /** Playback is buffering while paused. */\n  public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7;\n  /** Playback is buffering after a seek. */\n  public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8;\n  /** Playback is suppressed (e.g. due to audio focus loss). */\n  public static final int PLAYBACK_STATE_SUPPRESSED = 9;\n  /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */\n  public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10;\n  /** Playback has reached the end of the media. */\n  public static final int PLAYBACK_STATE_ENDED = 11;\n  /** Playback is stopped and can be restarted. */\n  public static final int PLAYBACK_STATE_STOPPED = 12;\n  /** Playback is stopped due a fatal error and can be retried. */\n  public static final int PLAYBACK_STATE_FAILED = 13;\n  /** Playback is interrupted by an ad. */\n  public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14;\n  /** Playback is abandoned before reaching the end of the media. */\n  public static final int PLAYBACK_STATE_ABANDONED = 15;\n  /** Total number of playback states. */\n  /* package */ static final int PLAYBACK_STATE_COUNT = 16;\n\n  /** Empty playback stats. */\n  public static final PlaybackStats EMPTY = merge(/* nothing */ );\n\n  /**\n   * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}.\n   *\n   * <p>Note that the full history of events is not kept as the history only makes sense in the\n   * context of a single playback.\n   *\n   * @param playbackStats Array of {@link PlaybackStats} to combine.\n   * @return The combined {@link PlaybackStats}.\n   */\n  public static PlaybackStats merge(PlaybackStats... playbackStats) {\n    int playbackCount = 0;\n    long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT];\n    long firstReportedTimeMs = C.TIME_UNSET;\n    int foregroundPlaybackCount = 0;\n    int abandonedBeforeReadyCount = 0;\n    int endedCount = 0;\n    int backgroundJoiningCount = 0;\n    long totalValidJoinTimeMs = C.TIME_UNSET;\n    int validJoinTimeCount = 0;\n    int totalPauseCount = 0;\n    int totalPauseBufferCount = 0;\n    int totalSeekCount = 0;\n    int totalRebufferCount = 0;\n    long maxRebufferTimeMs = C.TIME_UNSET;\n    int adPlaybackCount = 0;\n    long totalVideoFormatHeightTimeMs = 0;\n    long totalVideoFormatHeightTimeProduct = 0;\n    long totalVideoFormatBitrateTimeMs = 0;\n    long totalVideoFormatBitrateTimeProduct = 0;\n    long totalAudioFormatTimeMs = 0;\n    long totalAudioFormatBitrateTimeProduct = 0;\n    int initialVideoFormatHeightCount = 0;\n    int initialVideoFormatBitrateCount = 0;\n    int totalInitialVideoFormatHeight = C.LENGTH_UNSET;\n    long totalInitialVideoFormatBitrate = C.LENGTH_UNSET;\n    int initialAudioFormatBitrateCount = 0;\n    long totalInitialAudioFormatBitrate = C.LENGTH_UNSET;\n    long totalBandwidthTimeMs = 0;\n    long totalBandwidthBytes = 0;\n    long totalDroppedFrames = 0;\n    long totalAudioUnderruns = 0;\n    int fatalErrorPlaybackCount = 0;\n    int fatalErrorCount = 0;\n    int nonFatalErrorCount = 0;\n    for (PlaybackStats stats : playbackStats) {\n      playbackCount += stats.playbackCount;\n      for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {\n        playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i];\n      }\n      if (firstReportedTimeMs == C.TIME_UNSET) {\n        firstReportedTimeMs = stats.firstReportedTimeMs;\n      } else if (stats.firstReportedTimeMs != C.TIME_UNSET) {\n        firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs);\n      }\n      foregroundPlaybackCount += stats.foregroundPlaybackCount;\n      abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount;\n      endedCount += stats.endedCount;\n      backgroundJoiningCount += stats.backgroundJoiningCount;\n      if (totalValidJoinTimeMs == C.TIME_UNSET) {\n        totalValidJoinTimeMs = stats.totalValidJoinTimeMs;\n      } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) {\n        totalValidJoinTimeMs += stats.totalValidJoinTimeMs;\n      }\n      validJoinTimeCount += stats.validJoinTimeCount;\n      totalPauseCount += stats.totalPauseCount;\n      totalPauseBufferCount += stats.totalPauseBufferCount;\n      totalSeekCount += stats.totalSeekCount;\n      totalRebufferCount += stats.totalRebufferCount;\n      if (maxRebufferTimeMs == C.TIME_UNSET) {\n        maxRebufferTimeMs = stats.maxRebufferTimeMs;\n      } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) {\n        maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs);\n      }\n      adPlaybackCount += stats.adPlaybackCount;\n      totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs;\n      totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct;\n      totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs;\n      totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct;\n      totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs;\n      totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct;\n      initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount;\n      initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount;\n      if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) {\n        totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight;\n      } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) {\n        totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight;\n      }\n      if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) {\n        totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate;\n      } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) {\n        totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate;\n      }\n      initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount;\n      if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) {\n        totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate;\n      } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) {\n        totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate;\n      }\n      totalBandwidthTimeMs += stats.totalBandwidthTimeMs;\n      totalBandwidthBytes += stats.totalBandwidthBytes;\n      totalDroppedFrames += stats.totalDroppedFrames;\n      totalAudioUnderruns += stats.totalAudioUnderruns;\n      fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount;\n      fatalErrorCount += stats.fatalErrorCount;\n      nonFatalErrorCount += stats.nonFatalErrorCount;\n    }\n    return new PlaybackStats(\n        playbackCount,\n        playbackStateDurationsMs,\n        /* playbackStateHistory */ Collections.emptyList(),\n        /* mediaTimeHistory= */ Collections.emptyList(),\n        firstReportedTimeMs,\n        foregroundPlaybackCount,\n        abandonedBeforeReadyCount,\n        endedCount,\n        backgroundJoiningCount,\n        totalValidJoinTimeMs,\n        validJoinTimeCount,\n        totalPauseCount,\n        totalPauseBufferCount,\n        totalSeekCount,\n        totalRebufferCount,\n        maxRebufferTimeMs,\n        adPlaybackCount,\n        /* videoFormatHistory= */ Collections.emptyList(),\n        /* audioFormatHistory= */ Collections.emptyList(),\n        totalVideoFormatHeightTimeMs,\n        totalVideoFormatHeightTimeProduct,\n        totalVideoFormatBitrateTimeMs,\n        totalVideoFormatBitrateTimeProduct,\n        totalAudioFormatTimeMs,\n        totalAudioFormatBitrateTimeProduct,\n        initialVideoFormatHeightCount,\n        initialVideoFormatBitrateCount,\n        totalInitialVideoFormatHeight,\n        totalInitialVideoFormatBitrate,\n        initialAudioFormatBitrateCount,\n        totalInitialAudioFormatBitrate,\n        totalBandwidthTimeMs,\n        totalBandwidthBytes,\n        totalDroppedFrames,\n        totalAudioUnderruns,\n        fatalErrorPlaybackCount,\n        fatalErrorCount,\n        nonFatalErrorCount,\n        /* fatalErrorHistory= */ Collections.emptyList(),\n        /* nonFatalErrorHistory= */ Collections.emptyList());\n  }\n\n  /** The number of individual playbacks for which these stats were collected. */\n  public final int playbackCount;\n\n  // Playback state stats.\n\n  /**\n   * The playback state history as ordered pairs of the {@link EventTime} at which a state became\n   * active and the {@link PlaybackState}.\n   */\n  public final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;\n  /**\n   * The media time history as an ordered list of long[2] arrays with [0] being the realtime as\n   * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this\n   * realtime, in milliseconds.\n   */\n  public final List<long[]> mediaTimeHistory;\n  /**\n   * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first\n   * reported playback event, or {@link C#TIME_UNSET} if no event has been reported.\n   */\n  public final long firstReportedTimeMs;\n  /** The number of playbacks which were the active foreground playback at some point. */\n  public final int foregroundPlaybackCount;\n  /** The number of playbacks which were abandoned before they were ready to play. */\n  public final int abandonedBeforeReadyCount;\n  /** The number of playbacks which reached the ended state at least once. */\n  public final int endedCount;\n  /** The number of playbacks which were pre-buffered in the background. */\n  public final int backgroundJoiningCount;\n  /**\n   * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid\n   * join time could be determined.\n   *\n   * <p>Note that this does not include background joining time. A join time may be invalid if the\n   * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or\n   * joining was interrupted by a seek, stop, or error state.\n   */\n  public final long totalValidJoinTimeMs;\n  /**\n   * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}.\n   */\n  public final int validJoinTimeCount;\n  /** The total number of times a playback has been paused. */\n  public final int totalPauseCount;\n  /** The total number of times a playback has been paused while rebuffering. */\n  public final int totalPauseBufferCount;\n  /**\n   * The total number of times a seek occurred. This includes seeks happening before playback\n   * resumed after another seek.\n   */\n  public final int totalSeekCount;\n  /**\n   * The total number of times a rebuffer occurred. This excludes initial joining and buffering\n   * after seek.\n   */\n  public final int totalRebufferCount;\n  /**\n   * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no\n   * rebuffer occurred.\n   */\n  public final long maxRebufferTimeMs;\n  /** The number of ad playbacks. */\n  public final int adPlaybackCount;\n\n  // Format stats.\n\n  /**\n   * The video format history as ordered pairs of the {@link EventTime} at which a format started\n   * being used and the {@link Format}. The {@link Format} may be null if no video format was used.\n   */\n  public final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;\n  /**\n   * The audio format history as ordered pairs of the {@link EventTime} at which a format started\n   * being used and the {@link Format}. The {@link Format} may be null if no audio format was used.\n   */\n  public final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;\n  /** The total media time for which video format height data is available, in milliseconds. */\n  public final long totalVideoFormatHeightTimeMs;\n  /**\n   * The accumulated sum of all video format heights, in pixels, times the time the format was used\n   * for playback, in milliseconds.\n   */\n  public final long totalVideoFormatHeightTimeProduct;\n  /** The total media time for which video format bitrate data is available, in milliseconds. */\n  public final long totalVideoFormatBitrateTimeMs;\n  /**\n   * The accumulated sum of all video format bitrates, in bits per second, times the time the format\n   * was used for playback, in milliseconds.\n   */\n  public final long totalVideoFormatBitrateTimeProduct;\n  /** The total media time for which audio format data is available, in milliseconds. */\n  public final long totalAudioFormatTimeMs;\n  /**\n   * The accumulated sum of all audio format bitrates, in bits per second, times the time the format\n   * was used for playback, in milliseconds.\n   */\n  public final long totalAudioFormatBitrateTimeProduct;\n  /** The number of playbacks with initial video format height data. */\n  public final int initialVideoFormatHeightCount;\n  /** The number of playbacks with initial video format bitrate data. */\n  public final int initialVideoFormatBitrateCount;\n  /**\n   * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET}\n   * if no initial video format data is available.\n   */\n  public final int totalInitialVideoFormatHeight;\n  /**\n   * The total initial video format bitrate for all playbacks, in bits per second, or {@link\n   * C#LENGTH_UNSET} if no initial video format data is available.\n   */\n  public final long totalInitialVideoFormatBitrate;\n  /** The number of playbacks with initial audio format bitrate data. */\n  public final int initialAudioFormatBitrateCount;\n  /**\n   * The total initial audio format bitrate for all playbacks, in bits per second, or {@link\n   * C#LENGTH_UNSET} if no initial audio format data is available.\n   */\n  public final long totalInitialAudioFormatBitrate;\n\n  // Bandwidth stats.\n\n  /** The total time for which bandwidth measurement data is available, in milliseconds. */\n  public final long totalBandwidthTimeMs;\n  /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */\n  public final long totalBandwidthBytes;\n\n  // Renderer quality stats.\n\n  /** The total number of dropped video frames. */\n  public final long totalDroppedFrames;\n  /** The total number of audio underruns. */\n  public final long totalAudioUnderruns;\n\n  // Error stats.\n\n  /**\n   * The total number of playback with at least one fatal error. Errors are fatal if playback\n   * stopped due to this error.\n   */\n  public final int fatalErrorPlaybackCount;\n  /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */\n  public final int fatalErrorCount;\n  /**\n   * The total number of non-fatal errors. Error are non-fatal if playback can recover from the\n   * error without stopping.\n   */\n  public final int nonFatalErrorCount;\n  /**\n   * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error\n   * occurred and the error. Errors are fatal if playback stopped due to this error.\n   */\n  public final List<Pair<EventTime, Exception>> fatalErrorHistory;\n  /**\n   * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error\n   * occurred and the error. Error are non-fatal if playback can recover from the error without\n   * stopping.\n   */\n  public final List<Pair<EventTime, Exception>> nonFatalErrorHistory;\n\n  private final long[] playbackStateDurationsMs;\n\n  /* package */ PlaybackStats(\n      int playbackCount,\n      long[] playbackStateDurationsMs,\n      List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory,\n      List<long[]> mediaTimeHistory,\n      long firstReportedTimeMs,\n      int foregroundPlaybackCount,\n      int abandonedBeforeReadyCount,\n      int endedCount,\n      int backgroundJoiningCount,\n      long totalValidJoinTimeMs,\n      int validJoinTimeCount,\n      int totalPauseCount,\n      int totalPauseBufferCount,\n      int totalSeekCount,\n      int totalRebufferCount,\n      long maxRebufferTimeMs,\n      int adPlaybackCount,\n      List<Pair<EventTime, @NullableType Format>> videoFormatHistory,\n      List<Pair<EventTime, @NullableType Format>> audioFormatHistory,\n      long totalVideoFormatHeightTimeMs,\n      long totalVideoFormatHeightTimeProduct,\n      long totalVideoFormatBitrateTimeMs,\n      long totalVideoFormatBitrateTimeProduct,\n      long totalAudioFormatTimeMs,\n      long totalAudioFormatBitrateTimeProduct,\n      int initialVideoFormatHeightCount,\n      int initialVideoFormatBitrateCount,\n      int totalInitialVideoFormatHeight,\n      long totalInitialVideoFormatBitrate,\n      int initialAudioFormatBitrateCount,\n      long totalInitialAudioFormatBitrate,\n      long totalBandwidthTimeMs,\n      long totalBandwidthBytes,\n      long totalDroppedFrames,\n      long totalAudioUnderruns,\n      int fatalErrorPlaybackCount,\n      int fatalErrorCount,\n      int nonFatalErrorCount,\n      List<Pair<EventTime, Exception>> fatalErrorHistory,\n      List<Pair<EventTime, Exception>> nonFatalErrorHistory) {\n    this.playbackCount = playbackCount;\n    this.playbackStateDurationsMs = playbackStateDurationsMs;\n    this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory);\n    this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory);\n    this.firstReportedTimeMs = firstReportedTimeMs;\n    this.foregroundPlaybackCount = foregroundPlaybackCount;\n    this.abandonedBeforeReadyCount = abandonedBeforeReadyCount;\n    this.endedCount = endedCount;\n    this.backgroundJoiningCount = backgroundJoiningCount;\n    this.totalValidJoinTimeMs = totalValidJoinTimeMs;\n    this.validJoinTimeCount = validJoinTimeCount;\n    this.totalPauseCount = totalPauseCount;\n    this.totalPauseBufferCount = totalPauseBufferCount;\n    this.totalSeekCount = totalSeekCount;\n    this.totalRebufferCount = totalRebufferCount;\n    this.maxRebufferTimeMs = maxRebufferTimeMs;\n    this.adPlaybackCount = adPlaybackCount;\n    this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory);\n    this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory);\n    this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs;\n    this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct;\n    this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs;\n    this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct;\n    this.totalAudioFormatTimeMs = totalAudioFormatTimeMs;\n    this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct;\n    this.initialVideoFormatHeightCount = initialVideoFormatHeightCount;\n    this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount;\n    this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight;\n    this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate;\n    this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount;\n    this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate;\n    this.totalBandwidthTimeMs = totalBandwidthTimeMs;\n    this.totalBandwidthBytes = totalBandwidthBytes;\n    this.totalDroppedFrames = totalDroppedFrames;\n    this.totalAudioUnderruns = totalAudioUnderruns;\n    this.fatalErrorPlaybackCount = fatalErrorPlaybackCount;\n    this.fatalErrorCount = fatalErrorCount;\n    this.nonFatalErrorCount = nonFatalErrorCount;\n    this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory);\n    this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory);\n  }\n\n  /**\n   * Returns the total time spent in a given {@link PlaybackState}, in milliseconds.\n   *\n   * @param playbackState A {@link PlaybackState}.\n   * @return Total spent in the given playback state, in milliseconds\n   */\n  public long getPlaybackStateDurationMs(@PlaybackState int playbackState) {\n    return playbackStateDurationsMs[playbackState];\n  }\n\n  /**\n   * Returns the {@link PlaybackState} at the given time.\n   *\n   * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}.\n   * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the\n   *     given time is before the first known playback state in the history.\n   */\n  public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) {\n    @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED;\n    for (Pair<EventTime, @PlaybackState Integer> timeAndState : playbackStateHistory) {\n      if (timeAndState.first.realtimeMs > realtimeMs) {\n        break;\n      }\n      state = timeAndState.second;\n    }\n    return state;\n  }\n\n  /**\n   * Returns the estimated media time at the given realtime, in milliseconds, or {@link\n   * C#TIME_UNSET} if the media time history is unknown.\n   *\n   * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}.\n   * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no\n   *     estimate can be given.\n   */\n  public long getMediaTimeMsAtRealtimeMs(long realtimeMs) {\n    if (mediaTimeHistory.isEmpty()) {\n      return C.TIME_UNSET;\n    }\n    int nextIndex = 0;\n    while (nextIndex < mediaTimeHistory.size()\n        && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) {\n      nextIndex++;\n    }\n    if (nextIndex == 0) {\n      return mediaTimeHistory.get(0)[1];\n    }\n    if (nextIndex == mediaTimeHistory.size()) {\n      return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];\n    }\n    long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0];\n    long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1];\n    long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0];\n    long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1];\n    long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs;\n    if (realtimeDurationMs == 0) {\n      return prevMediaTimeMs;\n    }\n    float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs;\n    return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction);\n  }\n\n  /**\n   * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if\n   * no valid join time is available. Only includes playbacks with valid join times as documented in\n   * {@link #totalValidJoinTimeMs}.\n   */\n  public long getMeanJoinTimeMs() {\n    return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount;\n  }\n\n  /**\n   * Returns the total time spent joining the playback in foreground, in milliseconds. This does\n   * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or\n   * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state.\n   */\n  public long getTotalJoinTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND);\n  }\n\n  /** Returns the total time spent actively playing, in milliseconds. */\n  public long getTotalPlayTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING);\n  }\n\n  /**\n   * Returns the mean time spent actively playing per foreground playback, in milliseconds, or\n   * {@link C#TIME_UNSET} if no playback has been in foreground.\n   */\n  public long getMeanPlayTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalPlayTimeMs() / foregroundPlaybackCount;\n  }\n\n  /** Returns the total time spent in a paused state, in milliseconds. */\n  public long getTotalPausedTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED)\n        + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING);\n  }\n\n  /**\n   * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or\n   * {@link C#TIME_UNSET} if no playback has been in foreground.\n   */\n  public long getMeanPausedTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalPausedTimeMs() / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times,\n   * buffer times after a seek and buffering while paused.\n   */\n  public long getTotalRebufferTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING);\n  }\n\n  /**\n   * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link\n   * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer\n   * times after a seek and buffering while paused.\n   */\n  public long getMeanRebufferTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalRebufferTimeMs() / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET}\n   * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek.\n   */\n  public long getMeanSingleRebufferTimeMs() {\n    return totalRebufferCount == 0\n        ? C.TIME_UNSET\n        : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)\n                + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING))\n            / totalRebufferCount;\n  }\n\n  /**\n   * Returns the total time spent from the start of a seek until playback is ready again, in\n   * milliseconds.\n   */\n  public long getTotalSeekTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)\n        + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);\n  }\n\n  /**\n   * Returns the mean time spent per foreground playback from the start of a seek until playback is\n   * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.\n   */\n  public long getMeanSeekTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalSeekTimeMs() / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean time spent from the start of a single seek until playback is ready again, in\n   * milliseconds, or {@link C#TIME_UNSET} if no seek occurred.\n   */\n  public long getMeanSingleSeekTimeMs() {\n    return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount;\n  }\n\n  /**\n   * Returns the total time spent actively waiting for playback, in milliseconds. This includes all\n   * join times, rebuffer times and seek times, but excludes times without user intention to play,\n   * e.g. all paused states.\n   */\n  public long getTotalWaitTimeMs() {\n    return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND)\n        + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)\n        + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)\n        + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);\n  }\n\n  /**\n   * Returns the mean time spent actively waiting for playback per foreground playback, in\n   * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all\n   * join times, rebuffer times and seek times, but excludes times without user intention to play,\n   * e.g. all paused states.\n   */\n  public long getMeanWaitTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalWaitTimeMs() / foregroundPlaybackCount;\n  }\n\n  /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */\n  public long getTotalPlayAndWaitTimeMs() {\n    return getTotalPlayTimeMs() + getTotalWaitTimeMs();\n  }\n\n  /**\n   * Returns the mean time spent playing or actively waiting for playback per foreground playback,\n   * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.\n   */\n  public long getMeanPlayAndWaitTimeMs() {\n    return foregroundPlaybackCount == 0\n        ? C.TIME_UNSET\n        : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount;\n  }\n\n  /** Returns the total time covered by any playback state, in milliseconds. */\n  public long getTotalElapsedTimeMs() {\n    long totalTimeMs = 0;\n    for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {\n      totalTimeMs += playbackStateDurationsMs[i];\n    }\n    return totalTimeMs;\n  }\n\n  /**\n   * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link\n   * C#TIME_UNSET} if no playback was recorded.\n   */\n  public long getMeanElapsedTimeMs() {\n    return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount;\n  }\n\n  /**\n   * Returns the ratio of foreground playbacks which were abandoned before they were ready to play,\n   * or {@code 0.0} if no playback has been in foreground.\n   */\n  public float getAbandonedBeforeReadyRatio() {\n    int foregroundAbandonedBeforeReady =\n        abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount);\n    return foregroundPlaybackCount == 0\n        ? 0f\n        : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the ratio of foreground playbacks which reached the ended state at least once, or\n   * {@code 0.0} if no playback has been in foreground.\n   */\n  public float getEndedRatio() {\n    return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean number of times a playback has been paused per foreground playback, or {@code\n   * 0.0} if no playback has been in foreground.\n   */\n  public float getMeanPauseCount() {\n    return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean number of times a playback has been paused while rebuffering per foreground\n   * playback, or {@code 0.0} if no playback has been in foreground.\n   */\n  public float getMeanPauseBufferCount() {\n    return foregroundPlaybackCount == 0\n        ? 0f\n        : (float) totalPauseBufferCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no\n   * playback has been in foreground. This includes seeks happening before playback resumed after\n   * another seek.\n   */\n  public float getMeanSeekCount() {\n    return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if\n   * no playback has been in foreground. This excludes initial joining and buffering after seek.\n   */\n  public float getMeanRebufferCount() {\n    return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if\n   * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} /\n   * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link\n   * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}.\n   */\n  public float getWaitTimeRatio() {\n    long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();\n    return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs;\n  }\n\n  /**\n   * Returns the ratio of foreground join time to the total time spent playing and waiting, or\n   * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link\n   * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.\n   */\n  public float getJoinTimeRatio() {\n    long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();\n    return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs;\n  }\n\n  /**\n   * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0}\n   * if no time was spend playing or waiting. This is equivalent to {@link\n   * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.\n   */\n  public float getRebufferTimeRatio() {\n    long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();\n    return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs;\n  }\n\n  /**\n   * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if\n   * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} /\n   * {@link #getTotalPlayAndWaitTimeMs()}.\n   */\n  public float getSeekTimeRatio() {\n    long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();\n    return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs;\n  }\n\n  /**\n   * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no\n   * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}.\n   */\n  public float getRebufferRate() {\n    long playTimeMs = getTotalPlayTimeMs();\n    return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs;\n  }\n\n  /**\n   * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 /\n   * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.\n   */\n  public float getMeanTimeBetweenRebuffers() {\n    return 1f / getRebufferRate();\n  }\n\n  /**\n   * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video\n   * format data is available.\n   */\n  public int getMeanInitialVideoFormatHeight() {\n    return initialVideoFormatHeightCount == 0\n        ? C.LENGTH_UNSET\n        : totalInitialVideoFormatHeight / initialVideoFormatHeightCount;\n  }\n\n  /**\n   * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if\n   * no video format data is available.\n   */\n  public int getMeanInitialVideoFormatBitrate() {\n    return initialVideoFormatBitrateCount == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount);\n  }\n\n  /**\n   * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if\n   * no audio format data is available.\n   */\n  public int getMeanInitialAudioFormatBitrate() {\n    return initialAudioFormatBitrateCount == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount);\n  }\n\n  /**\n   * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format\n   * data is available. This is a weighted average taking the time the format was used for playback\n   * into account.\n   */\n  public int getMeanVideoFormatHeight() {\n    return totalVideoFormatHeightTimeMs == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs);\n  }\n\n  /**\n   * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no\n   * video format data is available. This is a weighted average taking the time the format was used\n   * for playback into account.\n   */\n  public int getMeanVideoFormatBitrate() {\n    return totalVideoFormatBitrateTimeMs == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs);\n  }\n\n  /**\n   * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no\n   * audio format data is available. This is a weighted average taking the time the format was used\n   * for playback into account.\n   */\n  public int getMeanAudioFormatBitrate() {\n    return totalAudioFormatTimeMs == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs);\n  }\n\n  /**\n   * Returns the mean network bandwidth based on transfer measurements, in bits per second, or\n   * {@link C#LENGTH_UNSET} if no transfer data is available.\n   */\n  public int getMeanBandwidth() {\n    return totalBandwidthTimeMs == 0\n        ? C.LENGTH_UNSET\n        : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs);\n  }\n\n  /**\n   * Returns the mean rate at which video frames are dropped, in dropped frames per play time\n   * second, or {@code 0.0} if no time was spent playing.\n   */\n  public float getDroppedFramesRate() {\n    long playTimeMs = getTotalPlayTimeMs();\n    return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs;\n  }\n\n  /**\n   * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or\n   * {@code 0.0} if no time was spent playing.\n   */\n  public float getAudioUnderrunRate() {\n    long playTimeMs = getTotalPlayTimeMs();\n    return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs;\n  }\n\n  /**\n   * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no\n   * playback has been in foreground.\n   */\n  public float getFatalErrorRatio() {\n    return foregroundPlaybackCount == 0\n        ? 0f\n        : (float) fatalErrorPlaybackCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was\n   * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}.\n   */\n  public float getFatalErrorRate() {\n    long playTimeMs = getTotalPlayTimeMs();\n    return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs;\n  }\n\n  /**\n   * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link\n   * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.\n   */\n  public float getMeanTimeBetweenFatalErrors() {\n    return 1f / getFatalErrorRate();\n  }\n\n  /**\n   * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no\n   * playback has been in foreground.\n   */\n  public float getMeanNonFatalErrorCount() {\n    return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount;\n  }\n\n  /**\n   * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time\n   * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}.\n   */\n  public float getNonFatalErrorRate() {\n    long playTimeMs = getTotalPlayTimeMs();\n    return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs;\n  }\n\n  /**\n   * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 /\n   * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.\n   */\n  public float getMeanTimeBetweenNonFatalErrors() {\n    return 1f / getNonFatalErrorRate();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.analytics;\n\nimport android.os.SystemClock;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player.\n *\n * <p>For accurate measurements, the listener should be added to the player before loading media,\n * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}.\n *\n * <p>Playback stats are gathered separately for all playback session, i.e. each window in the\n * {@link Timeline} and each single ad.\n */\npublic final class PlaybackStatsListener\n    implements AnalyticsListener, PlaybackSessionManager.Listener {\n\n  /** A listener for {@link PlaybackStats} updates. */\n  public interface Callback {\n\n    /**\n     * Called when a playback session ends and its {@link PlaybackStats} are ready.\n     *\n     * @param eventTime The {@link EventTime} at which the playback session started. Can be used to\n     *     identify the playback session.\n     * @param playbackStats The {@link PlaybackStats} for the ended playback session.\n     */\n    void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats);\n  }\n\n  private final PlaybackSessionManager sessionManager;\n  private final Map<String, PlaybackStatsTracker> playbackStatsTrackers;\n  private final Map<String, EventTime> sessionStartEventTimes;\n  @Nullable private final Callback callback;\n  private final boolean keepHistory;\n  private final Period period;\n\n  private PlaybackStats finishedPlaybackStats;\n  @Nullable private String activeContentPlayback;\n  @Nullable private String activeAdPlayback;\n  private boolean playWhenReady;\n  @Player.State private int playbackState;\n  private boolean isSuppressed;\n  private float playbackSpeed;\n\n  /**\n   * Creates listener for playback stats.\n   *\n   * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of\n   *     events.\n   * @param callback An optional callback for finished {@link PlaybackStats}.\n   */\n  public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) {\n    this.callback = callback;\n    this.keepHistory = keepHistory;\n    sessionManager = new DefaultPlaybackSessionManager();\n    playbackStatsTrackers = new HashMap<>();\n    sessionStartEventTimes = new HashMap<>();\n    finishedPlaybackStats = PlaybackStats.EMPTY;\n    playWhenReady = false;\n    playbackState = Player.STATE_IDLE;\n    playbackSpeed = 1f;\n    period = new Period();\n    sessionManager.setListener(this);\n  }\n\n  /**\n   * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is\n   * listening to.\n   *\n   * <p>Note that these {@link PlaybackStats} will not contain the full history of events.\n   *\n   * @return The combined {@link PlaybackStats} for all playback sessions.\n   */\n  public PlaybackStats getCombinedPlaybackStats() {\n    PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1];\n    allPendingPlaybackStats[0] = finishedPlaybackStats;\n    int index = 1;\n    for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {\n      allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false);\n    }\n    return PlaybackStats.merge(allPendingPlaybackStats);\n  }\n\n  /**\n   * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is\n   * active.\n   *\n   * @return {@link PlaybackStats} for the current playback session.\n   */\n  @Nullable\n  public PlaybackStats getPlaybackStats() {\n    PlaybackStatsTracker activeStatsTracker =\n        activeAdPlayback != null\n            ? playbackStatsTrackers.get(activeAdPlayback)\n            : activeContentPlayback != null\n                ? playbackStatsTrackers.get(activeContentPlayback)\n                : null;\n    return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false);\n  }\n\n  /**\n   * Finishes all pending playback sessions. Should be called when the listener is removed from the\n   * player or when the player is released.\n   */\n  public void finishAllSessions() {\n    // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with\n    // an actual EventTime. Should also simplify other cases where the listener needs to be released\n    // separately from the player.\n    HashMap<String, PlaybackStatsTracker> trackerCopy = new HashMap<>(playbackStatsTrackers);\n    EventTime dummyEventTime =\n        new EventTime(\n            SystemClock.elapsedRealtime(),\n            Timeline.EMPTY,\n            /* windowIndex= */ 0,\n            /* mediaPeriodId= */ null,\n            /* eventPlaybackPositionMs= */ 0,\n            /* currentPlaybackPositionMs= */ 0,\n            /* totalBufferedDurationMs= */ 0);\n    for (String session : trackerCopy.keySet()) {\n      onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false);\n    }\n  }\n\n  // PlaybackSessionManager.Listener implementation.\n\n  @Override\n  public void onSessionCreated(EventTime eventTime, String session) {\n    PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);\n    tracker.onPlayerStateChanged(\n        eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);\n    tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);\n    tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);\n    playbackStatsTrackers.put(session, tracker);\n    sessionStartEventTimes.put(session, eventTime);\n  }\n\n  @Override\n  public void onSessionActive(EventTime eventTime, String session) {\n    Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime);\n    if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {\n      activeAdPlayback = session;\n    } else {\n      activeContentPlayback = session;\n    }\n  }\n\n  @Override\n  public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {\n    Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());\n    long contentPositionUs =\n        eventTime\n            .timeline\n            .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)\n            .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);\n    EventTime contentEventTime =\n        new EventTime(\n            eventTime.realtimeMs,\n            eventTime.timeline,\n            eventTime.windowIndex,\n            new MediaPeriodId(\n                eventTime.mediaPeriodId.periodUid,\n                eventTime.mediaPeriodId.windowSequenceNumber,\n                eventTime.mediaPeriodId.adGroupIndex),\n            /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs),\n            eventTime.currentPlaybackPositionMs,\n            eventTime.totalBufferedDurationMs);\n    Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))\n        .onInterruptedByAd(contentEventTime);\n  }\n\n  @Override\n  public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) {\n    if (session.equals(activeAdPlayback)) {\n      activeAdPlayback = null;\n    } else if (session.equals(activeContentPlayback)) {\n      activeContentPlayback = null;\n    }\n    PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session));\n    EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session));\n    if (automaticTransition) {\n      // Simulate ENDED state to record natural ending of playback.\n      tracker.onPlayerStateChanged(\n          eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false);\n    }\n    tracker.onFinished(eventTime);\n    PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);\n    finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);\n    if (callback != null) {\n      callback.onPlaybackStatsReady(startEventTime, playbackStats);\n    }\n  }\n\n  // AnalyticsListener implementation.\n\n  @Override\n  public void onPlayerStateChanged(\n      EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {\n    this.playWhenReady = playWhenReady;\n    this.playbackState = playbackState;\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);\n      playbackStatsTrackers\n          .get(session)\n          .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback);\n    }\n  }\n\n  @Override\n  public void onPlaybackSuppressionReasonChanged(\n      EventTime eventTime, int playbackSuppressionReason) {\n    isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE;\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);\n      playbackStatsTrackers\n          .get(session)\n          .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback);\n    }\n  }\n\n  @Override\n  public void onTimelineChanged(EventTime eventTime, int reason) {\n    sessionManager.handleTimelineUpdate(eventTime);\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public void onPositionDiscontinuity(EventTime eventTime, int reason) {\n    sessionManager.handlePositionDiscontinuity(eventTime, reason);\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public void onSeekStarted(EventTime eventTime) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onSeekStarted(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public void onSeekProcessed(EventTime eventTime) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onSeekProcessed(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onFatalError(eventTime, error);\n      }\n    }\n  }\n\n  @Override\n  public void onPlaybackParametersChanged(\n      EventTime eventTime, PlaybackParameters playbackParameters) {\n    playbackSpeed = playbackParameters.speed;\n    sessionManager.updateSessions(eventTime);\n    for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {\n      tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);\n    }\n  }\n\n  @Override\n  public void onTracksChanged(\n      EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections);\n      }\n    }\n  }\n\n  @Override\n  public void onLoadStarted(\n      EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onLoadStarted(eventTime);\n      }\n    }\n  }\n\n  @Override\n  public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData);\n      }\n    }\n  }\n\n  @Override\n  public void onVideoSizeChanged(\n      EventTime eventTime,\n      int width,\n      int height,\n      int unappliedRotationDegrees,\n      float pixelWidthHeightRatio) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height);\n      }\n    }\n  }\n\n  @Override\n  public void onBandwidthEstimate(\n      EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded);\n      }\n    }\n  }\n\n  @Override\n  public void onAudioUnderrun(\n      EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onAudioUnderrun();\n      }\n    }\n  }\n\n  @Override\n  public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames);\n      }\n    }\n  }\n\n  @Override\n  public void onLoadError(\n      EventTime eventTime,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData,\n      IOException error,\n      boolean wasCanceled) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);\n      }\n    }\n  }\n\n  @Override\n  public void onDrmSessionManagerError(EventTime eventTime, Exception error) {\n    sessionManager.updateSessions(eventTime);\n    for (String session : playbackStatsTrackers.keySet()) {\n      if (sessionManager.belongsToSession(eventTime, session)) {\n        playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);\n      }\n    }\n  }\n\n  /** Tracker for playback stats of a single playback. */\n  private static final class PlaybackStatsTracker {\n\n    // Final stats.\n    private final boolean keepHistory;\n    private final long[] playbackStateDurationsMs;\n    private final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;\n    private final List<long[]> mediaTimeHistory;\n    private final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;\n    private final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;\n    private final List<Pair<EventTime, Exception>> fatalErrorHistory;\n    private final List<Pair<EventTime, Exception>> nonFatalErrorHistory;\n    private final boolean isAd;\n\n    private long firstReportedTimeMs;\n    private boolean hasBeenReady;\n    private boolean hasEnded;\n    private boolean isJoinTimeInvalid;\n    private int pauseCount;\n    private int pauseBufferCount;\n    private int seekCount;\n    private int rebufferCount;\n    private long maxRebufferTimeMs;\n    private int initialVideoFormatHeight;\n    private long initialVideoFormatBitrate;\n    private long initialAudioFormatBitrate;\n    private long videoFormatHeightTimeMs;\n    private long videoFormatHeightTimeProduct;\n    private long videoFormatBitrateTimeMs;\n    private long videoFormatBitrateTimeProduct;\n    private long audioFormatTimeMs;\n    private long audioFormatBitrateTimeProduct;\n    private long bandwidthTimeMs;\n    private long bandwidthBytes;\n    private long droppedFrames;\n    private long audioUnderruns;\n    private int fatalErrorCount;\n    private int nonFatalErrorCount;\n\n    // Current player state tracking.\n    private @PlaybackState int currentPlaybackState;\n    private long currentPlaybackStateStartTimeMs;\n    private boolean isSeeking;\n    private boolean isForeground;\n    private boolean isInterruptedByAd;\n    private boolean isFinished;\n    private boolean playWhenReady;\n    @Player.State private int playerPlaybackState;\n    private boolean isSuppressed;\n    private boolean hasFatalError;\n    private boolean startedLoading;\n    private long lastRebufferStartTimeMs;\n    @Nullable private Format currentVideoFormat;\n    @Nullable private Format currentAudioFormat;\n    private long lastVideoFormatStartTimeMs;\n    private long lastAudioFormatStartTimeMs;\n    private float currentPlaybackSpeed;\n\n    /**\n     * Creates a tracker for playback stats.\n     *\n     * @param keepHistory Whether to keep a full history of events.\n     * @param startTime The {@link EventTime} at which the playback stats start.\n     */\n    public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) {\n      this.keepHistory = keepHistory;\n      playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT];\n      playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();\n      currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED;\n      currentPlaybackStateStartTimeMs = startTime.realtimeMs;\n      playerPlaybackState = Player.STATE_IDLE;\n      firstReportedTimeMs = C.TIME_UNSET;\n      maxRebufferTimeMs = C.TIME_UNSET;\n      isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd();\n      initialAudioFormatBitrate = C.LENGTH_UNSET;\n      initialVideoFormatBitrate = C.LENGTH_UNSET;\n      initialVideoFormatHeight = C.LENGTH_UNSET;\n      currentPlaybackSpeed = 1f;\n    }\n\n    /**\n     * Notifies the tracker of a player state change event, including all player state changes while\n     * the playback is not in the foreground.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param playWhenReady Whether the playback will proceed when ready.\n     * @param playbackState The current {@link Player.State}.\n     * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.\n     */\n    public void onPlayerStateChanged(\n        EventTime eventTime,\n        boolean playWhenReady,\n        @Player.State int playbackState,\n        boolean belongsToPlayback) {\n      this.playWhenReady = playWhenReady;\n      playerPlaybackState = playbackState;\n      if (playbackState != Player.STATE_IDLE) {\n        hasFatalError = false;\n      }\n      if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {\n        isInterruptedByAd = false;\n      }\n      maybeUpdatePlaybackState(eventTime, belongsToPlayback);\n    }\n\n    /**\n     * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss),\n     * including all updates while the playback is not in the foreground.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param isSuppressed Whether playback is suppressed.\n     * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.\n     */\n    public void onIsSuppressedChanged(\n        EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) {\n      this.isSuppressed = isSuppressed;\n      maybeUpdatePlaybackState(eventTime, belongsToPlayback);\n    }\n\n    /**\n     * Notifies the tracker of a position discontinuity or timeline update for the current playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onPositionDiscontinuity(EventTime eventTime) {\n      isInterruptedByAd = false;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker of the start of a seek in the current playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onSeekStarted(EventTime eventTime) {\n      isSeeking = true;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker of a seek has been processed in the current playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onSeekProcessed(EventTime eventTime) {\n      isSeeking = false;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker of fatal player error in the current playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onFatalError(EventTime eventTime, Exception error) {\n      fatalErrorCount++;\n      if (keepHistory) {\n        fatalErrorHistory.add(Pair.create(eventTime, error));\n      }\n      hasFatalError = true;\n      isInterruptedByAd = false;\n      isSeeking = false;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker that a load for the current playback has started.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onLoadStarted(EventTime eventTime) {\n      startedLoading = true;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker that the current playback became the active foreground playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onForeground(EventTime eventTime) {\n      isForeground = true;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker that the current playback has been interrupted for ad playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     */\n    public void onInterruptedByAd(EventTime eventTime) {\n      isInterruptedByAd = true;\n      isSeeking = false;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);\n    }\n\n    /**\n     * Notifies the tracker that the current playback has finished.\n     *\n     * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback.\n     */\n    public void onFinished(EventTime eventTime) {\n      isFinished = true;\n      maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false);\n    }\n\n    /**\n     * Notifies the tracker that the track selection for the current playback changed.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param trackSelections The new {@link TrackSelectionArray}.\n     */\n    public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) {\n      boolean videoEnabled = false;\n      boolean audioEnabled = false;\n      for (TrackSelection trackSelection : trackSelections.getAll()) {\n        if (trackSelection != null && trackSelection.length() > 0) {\n          int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType);\n          if (trackType == C.TRACK_TYPE_VIDEO) {\n            videoEnabled = true;\n          } else if (trackType == C.TRACK_TYPE_AUDIO) {\n            audioEnabled = true;\n          }\n        }\n      }\n      if (!videoEnabled) {\n        maybeUpdateVideoFormat(eventTime, /* newFormat= */ null);\n      }\n      if (!audioEnabled) {\n        maybeUpdateAudioFormat(eventTime, /* newFormat= */ null);\n      }\n    }\n\n    /**\n     * Notifies the tracker that a format being read by the renderers for the current playback\n     * changed.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param mediaLoadData The {@link MediaLoadData} describing the format change.\n     */\n    public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {\n      if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO\n          || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) {\n        maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat);\n      } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {\n        maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat);\n      }\n    }\n\n    /**\n     * Notifies the tracker that the video size for the current playback changed.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param width The video width in pixels.\n     * @param height The video height in pixels.\n     */\n    public void onVideoSizeChanged(EventTime eventTime, int width, int height) {\n      if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) {\n        Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height);\n        maybeUpdateVideoFormat(eventTime, formatWithHeight);\n      }\n    }\n\n    /**\n     * Notifies the tracker of a playback speed change, including all playback speed changes while\n     * the playback is not in the foreground.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param playbackSpeed The new playback speed.\n     */\n    public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {\n      maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs);\n      maybeRecordVideoFormatTime(eventTime.realtimeMs);\n      maybeRecordAudioFormatTime(eventTime.realtimeMs);\n      currentPlaybackSpeed = playbackSpeed;\n    }\n\n    /** Notifies the builder of an audio underrun for the current playback. */\n    public void onAudioUnderrun() {\n      audioUnderruns++;\n    }\n\n    /**\n     * Notifies the tracker of dropped video frames for the current playback.\n     *\n     * @param droppedFrames The number of dropped video frames.\n     */\n    public void onDroppedVideoFrames(int droppedFrames) {\n      this.droppedFrames += droppedFrames;\n    }\n\n    /**\n     * Notifies the tracker of bandwidth measurement data for the current playback.\n     *\n     * @param timeMs The time for which bandwidth measurement data is available, in milliseconds.\n     * @param bytes The bytes transferred during {@code timeMs}.\n     */\n    public void onBandwidthData(long timeMs, long bytes) {\n      bandwidthTimeMs += timeMs;\n      bandwidthBytes += bytes;\n    }\n\n    /**\n     * Notifies the tracker of a non-fatal error in the current playback.\n     *\n     * @param eventTime The {@link EventTime}.\n     * @param error The error.\n     */\n    public void onNonFatalError(EventTime eventTime, Exception error) {\n      nonFatalErrorCount++;\n      if (keepHistory) {\n        nonFatalErrorHistory.add(Pair.create(eventTime, error));\n      }\n    }\n\n    /**\n     * Builds the playback stats.\n     *\n     * @param isFinal Whether this is the final build and no further events are expected.\n     */\n    public PlaybackStats build(boolean isFinal) {\n      long[] playbackStateDurationsMs = this.playbackStateDurationsMs;\n      List<long[]> mediaTimeHistory = this.mediaTimeHistory;\n      if (!isFinal) {\n        long buildTimeMs = SystemClock.elapsedRealtime();\n        playbackStateDurationsMs =\n            Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT);\n        long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs);\n        playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs;\n        maybeUpdateMaxRebufferTimeMs(buildTimeMs);\n        maybeRecordVideoFormatTime(buildTimeMs);\n        maybeRecordAudioFormatTime(buildTimeMs);\n        mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory);\n        if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) {\n          mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs));\n        }\n      }\n      boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady;\n      long validJoinTimeMs =\n          isJoinTimeInvalid\n              ? C.TIME_UNSET\n              : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND];\n      boolean hasBackgroundJoin =\n          playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0;\n      List<Pair<EventTime, @NullableType Format>> videoHistory =\n          isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory);\n      List<Pair<EventTime, @NullableType Format>> audioHistory =\n          isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory);\n      return new PlaybackStats(\n          /* playbackCount= */ 1,\n          playbackStateDurationsMs,\n          isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory),\n          mediaTimeHistory,\n          firstReportedTimeMs,\n          /* foregroundPlaybackCount= */ isForeground ? 1 : 0,\n          /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1,\n          /* endedCount= */ hasEnded ? 1 : 0,\n          /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0,\n          validJoinTimeMs,\n          /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1,\n          pauseCount,\n          pauseBufferCount,\n          seekCount,\n          rebufferCount,\n          maxRebufferTimeMs,\n          /* adPlaybackCount= */ isAd ? 1 : 0,\n          videoHistory,\n          audioHistory,\n          videoFormatHeightTimeMs,\n          videoFormatHeightTimeProduct,\n          videoFormatBitrateTimeMs,\n          videoFormatBitrateTimeProduct,\n          audioFormatTimeMs,\n          audioFormatBitrateTimeProduct,\n          /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1,\n          /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1,\n          initialVideoFormatHeight,\n          initialVideoFormatBitrate,\n          /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1,\n          initialAudioFormatBitrate,\n          bandwidthTimeMs,\n          bandwidthBytes,\n          droppedFrames,\n          audioUnderruns,\n          /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0,\n          fatalErrorCount,\n          nonFatalErrorCount,\n          fatalErrorHistory,\n          nonFatalErrorHistory);\n    }\n\n    private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {\n      @PlaybackState int newPlaybackState = resolveNewPlaybackState();\n      if (newPlaybackState == currentPlaybackState) {\n        return;\n      }\n      Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs);\n\n      long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs;\n      playbackStateDurationsMs[currentPlaybackState] += stateDurationMs;\n      if (firstReportedTimeMs == C.TIME_UNSET) {\n        firstReportedTimeMs = eventTime.realtimeMs;\n      }\n      isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState);\n      hasBeenReady |= isReadyState(newPlaybackState);\n      hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED;\n      if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) {\n        pauseCount++;\n      }\n      if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) {\n        seekCount++;\n      }\n      if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) {\n        rebufferCount++;\n        lastRebufferStartTimeMs = eventTime.realtimeMs;\n      }\n      if (isRebufferingState(currentPlaybackState)\n          && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING\n          && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) {\n        pauseBufferCount++;\n      }\n\n      maybeUpdateMediaTimeHistory(\n          eventTime.realtimeMs,\n          /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET);\n      maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs);\n      maybeRecordVideoFormatTime(eventTime.realtimeMs);\n      maybeRecordAudioFormatTime(eventTime.realtimeMs);\n\n      currentPlaybackState = newPlaybackState;\n      currentPlaybackStateStartTimeMs = eventTime.realtimeMs;\n      if (keepHistory) {\n        playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState));\n      }\n    }\n\n    private @PlaybackState int resolveNewPlaybackState() {\n      if (isFinished) {\n        // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item).\n        return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED\n            ? PlaybackStats.PLAYBACK_STATE_ENDED\n            : PlaybackStats.PLAYBACK_STATE_ABANDONED;\n      } else if (isSeeking) {\n        // Seeking takes precedence over errors such that we report a seek while in error state.\n        return PlaybackStats.PLAYBACK_STATE_SEEKING;\n      } else if (hasFatalError) {\n        return PlaybackStats.PLAYBACK_STATE_FAILED;\n      } else if (!isForeground) {\n        // Before the playback becomes foreground, only report background joining and not started.\n        return startedLoading\n            ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND\n            : PlaybackStats.PLAYBACK_STATE_NOT_STARTED;\n      } else if (isInterruptedByAd) {\n        return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD;\n      } else if (playerPlaybackState == Player.STATE_ENDED) {\n        return PlaybackStats.PLAYBACK_STATE_ENDED;\n      } else if (playerPlaybackState == Player.STATE_BUFFERING) {\n        if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED\n            || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND\n            || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND\n            || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {\n          return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;\n        }\n        if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING\n            || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) {\n          return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING;\n        }\n        if (!playWhenReady) {\n          return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;\n        }\n        return isSuppressed\n            ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING\n            : PlaybackStats.PLAYBACK_STATE_BUFFERING;\n      } else if (playerPlaybackState == Player.STATE_READY) {\n        if (!playWhenReady) {\n          return PlaybackStats.PLAYBACK_STATE_PAUSED;\n        }\n        return isSuppressed\n            ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED\n            : PlaybackStats.PLAYBACK_STATE_PLAYING;\n      } else if (playerPlaybackState == Player.STATE_IDLE\n          && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) {\n        // This case only applies for calls to player.stop(). All other IDLE cases are handled by\n        // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.\n        return PlaybackStats.PLAYBACK_STATE_STOPPED;\n      }\n      return currentPlaybackState;\n    }\n\n    private void maybeUpdateMaxRebufferTimeMs(long nowMs) {\n      if (isRebufferingState(currentPlaybackState)) {\n        long rebufferDurationMs = nowMs - lastRebufferStartTimeMs;\n        if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) {\n          maxRebufferTimeMs = rebufferDurationMs;\n        }\n      }\n    }\n\n    private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) {\n      if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) {\n        if (mediaTimeMs == C.TIME_UNSET) {\n          return;\n        }\n        if (!mediaTimeHistory.isEmpty()) {\n          long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];\n          if (previousMediaTimeMs != mediaTimeMs) {\n            mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs});\n          }\n        }\n      }\n      mediaTimeHistory.add(\n          mediaTimeMs == C.TIME_UNSET\n              ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs)\n              : new long[] {realtimeMs, mediaTimeMs});\n    }\n\n    private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) {\n      long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1);\n      long previousRealtimeMs = previousKnownMediaTimeHistory[0];\n      long previousMediaTimeMs = previousKnownMediaTimeHistory[1];\n      long elapsedMediaTimeEstimateMs =\n          (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed);\n      long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs;\n      return new long[] {realtimeMs, mediaTimeEstimateMs};\n    }\n\n    private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) {\n      if (Util.areEqual(currentVideoFormat, newFormat)) {\n        return;\n      }\n      maybeRecordVideoFormatTime(eventTime.realtimeMs);\n      if (newFormat != null) {\n        if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) {\n          initialVideoFormatHeight = newFormat.height;\n        }\n        if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) {\n          initialVideoFormatBitrate = newFormat.bitrate;\n        }\n      }\n      currentVideoFormat = newFormat;\n      if (keepHistory) {\n        videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat));\n      }\n    }\n\n    private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) {\n      if (Util.areEqual(currentAudioFormat, newFormat)) {\n        return;\n      }\n      maybeRecordAudioFormatTime(eventTime.realtimeMs);\n      if (newFormat != null\n          && initialAudioFormatBitrate == C.LENGTH_UNSET\n          && newFormat.bitrate != Format.NO_VALUE) {\n        initialAudioFormatBitrate = newFormat.bitrate;\n      }\n      currentAudioFormat = newFormat;\n      if (keepHistory) {\n        audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat));\n      }\n    }\n\n    private void maybeRecordVideoFormatTime(long nowMs) {\n      if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING\n          && currentVideoFormat != null) {\n        long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed);\n        if (currentVideoFormat.height != Format.NO_VALUE) {\n          videoFormatHeightTimeMs += mediaDurationMs;\n          videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height;\n        }\n        if (currentVideoFormat.bitrate != Format.NO_VALUE) {\n          videoFormatBitrateTimeMs += mediaDurationMs;\n          videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate;\n        }\n      }\n      lastVideoFormatStartTimeMs = nowMs;\n    }\n\n    private void maybeRecordAudioFormatTime(long nowMs) {\n      if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING\n          && currentAudioFormat != null\n          && currentAudioFormat.bitrate != Format.NO_VALUE) {\n        long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed);\n        audioFormatTimeMs += mediaDurationMs;\n        audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate;\n      }\n      lastAudioFormatStartTimeMs = nowMs;\n    }\n\n    private static boolean isReadyState(@PlaybackState int state) {\n      return state == PlaybackStats.PLAYBACK_STATE_PLAYING\n          || state == PlaybackStats.PLAYBACK_STATE_PAUSED\n          || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED;\n    }\n\n    private static boolean isPausedState(@PlaybackState int state) {\n      return state == PlaybackStats.PLAYBACK_STATE_PAUSED\n          || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;\n    }\n\n    private static boolean isRebufferingState(@PlaybackState int state) {\n      return state == PlaybackStats.PLAYBACK_STATE_BUFFERING\n          || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING\n          || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING;\n    }\n\n    private static boolean isInvalidJoinTransition(\n        @PlaybackState int oldState, @PlaybackState int newState) {\n      if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND\n          && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND\n          && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {\n        return false;\n      }\n      return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND\n          && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND\n          && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD\n          && newState != PlaybackStats.PLAYBACK_STATE_PLAYING\n          && newState != PlaybackStats.PLAYBACK_STATE_PAUSED\n          && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED\n          && newState != PlaybackStats.PLAYBACK_STATE_ENDED;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/analytics/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.analytics;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo.StreamType;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\n\n/**\n * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the\n * definition in ETSI TS 102 366 V1.2.1.\n */\npublic final class Ac3Util {\n\n  /** Holds sample format information as presented by a syncframe header. */\n  public static final class SyncFrameInfo {\n\n    /**\n     * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED},\n     * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.\n     */\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({STREAM_TYPE_UNDEFINED, STREAM_TYPE_TYPE0, STREAM_TYPE_TYPE1, STREAM_TYPE_TYPE2})\n    public @interface StreamType {}\n    /** Undefined AC3 stream type. */\n    public static final int STREAM_TYPE_UNDEFINED = -1;\n    /** Type 0 AC3 stream type. */\n    public static final int STREAM_TYPE_TYPE0 = 0;\n    /** Type 1 AC3 stream type. */\n    public static final int STREAM_TYPE_TYPE1 = 1;\n    /** Type 2 AC3 stream type. */\n    public static final int STREAM_TYPE_TYPE2 = 2;\n\n    /**\n     * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and {@link\n     * MimeTypes#AUDIO_E_AC3}.\n     */\n    @Nullable public final String mimeType;\n    /**\n     * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or {@link\n     * #STREAM_TYPE_UNDEFINED} otherwise.\n     */\n    public final @StreamType int streamType;\n    /**\n     * The audio sampling rate in Hz.\n     */\n    public final int sampleRate;\n    /**\n     * The number of audio channels\n     */\n    public final int channelCount;\n    /**\n     * The size of the frame.\n     */\n    public final int frameSize;\n    /**\n     * Number of audio samples in the frame.\n     */\n    public final int sampleCount;\n\n    private SyncFrameInfo(\n        @Nullable String mimeType,\n        @StreamType int streamType,\n        int channelCount,\n        int sampleRate,\n        int frameSize,\n        int sampleCount) {\n      this.mimeType = mimeType;\n      this.streamType = streamType;\n      this.channelCount = channelCount;\n      this.sampleRate = sampleRate;\n      this.frameSize = frameSize;\n      this.sampleCount = sampleCount;\n    }\n\n  }\n\n  /**\n   * The number of samples to store in each output chunk when rechunking TrueHD streams. The number\n   * of samples extracted from the container corresponding to one syncframe must be an integer\n   * multiple of this value.\n   */\n  public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 16;\n  /**\n   * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count.\n   */\n  public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 10;\n\n  /**\n   * The number of new samples per (E-)AC-3 audio block.\n   */\n  private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;\n  /**\n   * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.\n   */\n  private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK;\n  /**\n   * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod.\n   */\n  private static final int[] BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD = new int[] {1, 2, 3, 6};\n  /**\n   * Sample rates, indexed by fscod.\n   */\n  private static final int[] SAMPLE_RATE_BY_FSCOD = new int[] {48000, 44100, 32000};\n  /**\n   * Sample rates, indexed by fscod2 (E-AC-3).\n   */\n  private static final int[] SAMPLE_RATE_BY_FSCOD2 = new int[] {24000, 22050, 16000};\n  /**\n   * Channel counts, indexed by acmod.\n   */\n  private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5};\n  /**\n   * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)\n   */\n  private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96,\n      112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640};\n  /**\n   * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)\n   */\n  private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104,\n      121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};\n\n  /**\n   * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS\n   * 102 366 Annex F. The reading position of {@code data} will be modified.\n   *\n   * @param data The AC3SpecificBox to parse.\n   * @param trackId The track identifier to set on the format.\n   * @param language The language to set on the format.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @return The AC-3 format parsed from data in the header.\n   */\n  public static Format parseAc3AnnexFFormat(\n      ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) {\n    int fscod = (data.readUnsignedByte() & 0xC0) >> 6;\n    int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];\n    int nextByte = data.readUnsignedByte();\n    int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x38) >> 3];\n    if ((nextByte & 0x04) != 0) { // lfeon\n      channelCount++;\n    }\n    return Format.createAudioSampleFormat(\n        trackId,\n        MimeTypes.AUDIO_AC3,\n        /* codecs= */ null,\n        Format.NO_VALUE,\n        Format.NO_VALUE,\n        channelCount,\n        sampleRate,\n        /* initializationData= */ null,\n        drmInitData,\n        /* selectionFlags= */ 0,\n        language);\n  }\n\n  /**\n   * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS\n   * 102 366 Annex F. The reading position of {@code data} will be modified.\n   *\n   * @param data The EC3SpecificBox to parse.\n   * @param trackId The track identifier to set on the format.\n   * @param language The language to set on the format.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @return The E-AC-3 format parsed from data in the header.\n   */\n  public static Format parseEAc3AnnexFFormat(\n      ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) {\n    data.skipBytes(2); // data_rate, num_ind_sub\n\n    // Read the first independent substream.\n    int fscod = (data.readUnsignedByte() & 0xC0) >> 6;\n    int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];\n    int nextByte = data.readUnsignedByte();\n    int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x0E) >> 1];\n    if ((nextByte & 0x01) != 0) { // lfeon\n      channelCount++;\n    }\n\n    // Read the first dependent substream.\n    nextByte = data.readUnsignedByte();\n    int numDepSub = ((nextByte & 0x1E) >> 1);\n    if (numDepSub > 0) {\n      int lowByteChanLoc = data.readUnsignedByte();\n      // Read Lrs/Rrs pair\n      // TODO: Read other channel configuration\n      if ((lowByteChanLoc & 0x02) != 0) {\n        channelCount += 2;\n      }\n    }\n    String mimeType = MimeTypes.AUDIO_E_AC3;\n    if (data.bytesLeft() > 0) {\n      nextByte = data.readUnsignedByte();\n      if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a\n        mimeType = MimeTypes.AUDIO_E_AC3_JOC;\n      }\n    }\n    return Format.createAudioSampleFormat(\n        trackId,\n        mimeType,\n        /* codecs= */ null,\n        Format.NO_VALUE,\n        Format.NO_VALUE,\n        channelCount,\n        sampleRate,\n        /* initializationData= */ null,\n        drmInitData,\n        /* selectionFlags= */ 0,\n        language);\n  }\n\n  /**\n   * Returns (E-)AC-3 format information given {@code data} containing a syncframe. The reading\n   * position of {@code data} will be modified.\n   *\n   * @param data The data to parse, positioned at the start of the syncframe.\n   * @return The (E-)AC-3 format data parsed from the header.\n   */\n  public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {\n    int initialPosition = data.getPosition();\n    data.skipBits(40);\n    boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6.\n    data.setPosition(initialPosition);\n    String mimeType;\n    @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;\n    int sampleRate;\n    int acmod;\n    int frameSize;\n    int sampleCount;\n    boolean lfeon;\n    int channelCount;\n    if (isEac3) {\n      // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2.\n      data.skipBits(16); // syncword\n      switch (data.readBits(2)) { // strmtyp\n        case 0:\n          streamType = SyncFrameInfo.STREAM_TYPE_TYPE0;\n          break;\n        case 1:\n          streamType = SyncFrameInfo.STREAM_TYPE_TYPE1;\n          break;\n        case 2:\n          streamType = SyncFrameInfo.STREAM_TYPE_TYPE2;\n          break;\n        default:\n          streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;\n          break;\n      }\n      data.skipBits(3); // substreamid\n      frameSize = (data.readBits(11) + 1) * 2; // See frmsiz in subsection E.1.3.1.3.\n      int fscod = data.readBits(2);\n      int audioBlocks;\n      int numblkscod;\n      if (fscod == 3) {\n        numblkscod = 3;\n        sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];\n        audioBlocks = 6;\n      } else {\n        numblkscod = data.readBits(2);\n        audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod];\n        sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];\n      }\n      sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks;\n      acmod = data.readBits(3);\n      lfeon = data.readBit();\n      channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);\n      data.skipBits(5 + 5); // bsid, dialnorm\n      if (data.readBit()) { // compre\n        data.skipBits(8); // compr\n      }\n      if (acmod == 0) {\n        data.skipBits(5); // dialnorm2\n        if (data.readBit()) { // compr2e\n          data.skipBits(8); // compr2\n        }\n      }\n      if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape\n        data.skipBits(16); // chanmap\n      }\n      if (data.readBit()) { // mixmdate\n        if (acmod > 2) {\n          data.skipBits(2); // dmixmod\n        }\n        if ((acmod & 0x01) != 0 && acmod > 2) {\n          data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev\n        }\n        if ((acmod & 0x04) != 0) {\n          data.skipBits(6); // ltrtsurmixlev, lorosurmixlev\n        }\n        if (lfeon && data.readBit()) { // lfemixlevcode\n          data.skipBits(5); // lfemixlevcod\n        }\n        if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0) {\n          if (data.readBit()) { // pgmscle\n            data.skipBits(6); //pgmscl\n          }\n          if (acmod == 0 && data.readBit()) { // pgmscl2e\n            data.skipBits(6); // pgmscl2\n          }\n          if (data.readBit()) { // extpgmscle\n            data.skipBits(6); // extpgmscl\n          }\n          int mixdef = data.readBits(2);\n          if (mixdef == 1) {\n            data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl\n          } else if (mixdef == 2) {\n            data.skipBits(12); // mixdata\n          } else if (mixdef == 3) {\n            int mixdeflen = data.readBits(5);\n            if (data.readBit()) { // mixdata2e\n              data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl\n              if (data.readBit()) { // extpgmlscle\n                data.skipBits(4); // extpgmlscl\n              }\n              if (data.readBit()) { // extpgmcscle\n                data.skipBits(4); // extpgmcscl\n              }\n              if (data.readBit()) { // extpgmrscle\n                data.skipBits(4); // extpgmrscl\n              }\n              if (data.readBit()) { // extpgmlsscle\n                data.skipBits(4); // extpgmlsscl\n              }\n              if (data.readBit()) { // extpgmrsscle\n                data.skipBits(4); // extpgmrsscl\n              }\n              if (data.readBit()) { // extpgmlfescle\n                data.skipBits(4); // extpgmlfescl\n              }\n              if (data.readBit()) { // dmixscle\n                data.skipBits(4); // dmixscl\n              }\n              if (data.readBit()) { // addche\n                if (data.readBit()) { // extpgmaux1scle\n                  data.skipBits(4); // extpgmaux1scl\n                }\n                if (data.readBit()) { // extpgmaux2scle\n                  data.skipBits(4); // extpgmaux2scl\n                }\n              }\n            }\n            if (data.readBit()) { // mixdata3e\n              data.skipBits(5); // spchdat\n              if (data.readBit()) { // addspchdate\n                data.skipBits(5 + 2); // spchdat1, spchan1att\n                if (data.readBit()) { // addspdat1e\n                  data.skipBits(5 + 3); // spchdat2, spchan2att\n                }\n              }\n            }\n            data.skipBits(8 * (mixdeflen + 2)); // mixdata\n            data.byteAlign(); // mixdatafill\n          }\n          if (acmod < 2) {\n            if (data.readBit()) { // paninfoe\n              data.skipBits(8 + 6); // panmean, paninfo\n            }\n            if (acmod == 0) {\n              if (data.readBit()) { // paninfo2e\n                data.skipBits(8 + 6); // panmean2, paninfo2\n              }\n            }\n          }\n          if (data.readBit()) { // frmmixcfginfoe\n            if (numblkscod == 0) {\n              data.skipBits(5); // blkmixcfginfo[0]\n            } else {\n              for (int blk = 0; blk < audioBlocks; blk++) {\n                if (data.readBit()) { // blkmixcfginfoe\n                  data.skipBits(5); // blkmixcfginfo[blk]\n                }\n              }\n            }\n          }\n        }\n      }\n      if (data.readBit()) { // infomdate\n        data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs\n        if (acmod == 2) {\n          data.skipBits(2 + 2); // dsurmod, dheadphonmod\n        }\n        if (acmod >= 6) {\n          data.skipBits(2); // dsurexmod\n        }\n        if (data.readBit()) { // audioprodie\n          data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp\n        }\n        if (acmod == 0 && data.readBit()) { // audioprodi2e\n          data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2\n        }\n        if (fscod < 3) {\n          data.skipBit(); // sourcefscod\n        }\n      }\n      if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0 && numblkscod != 3) {\n        data.skipBit(); // convsync\n      }\n      if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE2\n          && (numblkscod == 3 || data.readBit())) { // blkid\n        data.skipBits(6); // frmsizecod\n      }\n      mimeType = MimeTypes.AUDIO_E_AC3;\n      if (data.readBit()) { // addbsie\n        int addbsil = data.readBits(6);\n        if (addbsil == 1 && data.readBits(8) == 1) { // addbsi\n          mimeType = MimeTypes.AUDIO_E_AC3_JOC;\n        }\n      }\n    } else /* is AC-3 */ {\n      mimeType = MimeTypes.AUDIO_AC3;\n      data.skipBits(16 + 16); // syncword, crc1\n      int fscod = data.readBits(2);\n      if (fscod == 3) {\n        // fscod '11' indicates that the decoder should not attempt to decode audio. We invalidate\n        // the mime type to prevent association with a renderer.\n        mimeType = null;\n      }\n      int frmsizecod = data.readBits(6);\n      frameSize = getAc3SyncframeSize(fscod, frmsizecod);\n      data.skipBits(5 + 3); // bsid, bsmod\n      acmod = data.readBits(3);\n      if ((acmod & 0x01) != 0 && acmod != 1) {\n        data.skipBits(2); // cmixlev\n      }\n      if ((acmod & 0x04) != 0) {\n        data.skipBits(2); // surmixlev\n      }\n      if (acmod == 2) {\n        data.skipBits(2); // dsurmod\n      }\n      sampleRate =\n          fscod < SAMPLE_RATE_BY_FSCOD.length ? SAMPLE_RATE_BY_FSCOD[fscod] : Format.NO_VALUE;\n      sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;\n      lfeon = data.readBit();\n      channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);\n    }\n    return new SyncFrameInfo(\n        mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount);\n  }\n\n  /**\n   * Returns the size in bytes of the given (E-)AC-3 syncframe.\n   *\n   * @param data The syncframe to parse.\n   * @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid.\n   */\n  public static int parseAc3SyncframeSize(byte[] data) {\n    if (data.length < 6) {\n      return C.LENGTH_UNSET;\n    }\n    boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6.\n    if (isEac3) {\n      int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits.\n      frmsiz |= data[3] & 0xFF; // Least significant 8 bits.\n      return (frmsiz + 1) * 2; // See frmsiz in subsection E.1.3.1.3.\n    } else {\n      int fscod = (data[4] & 0xC0) >> 6;\n      int frmsizecod = data[4] & 0x3F;\n      return getAc3SyncframeSize(fscod, frmsizecod);\n    }\n  }\n\n  /**\n   * Returns the number of audio samples in an AC-3 syncframe.\n   */\n  public static int getAc3SyncframeAudioSampleCount() {\n    return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;\n  }\n\n  /**\n   * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's\n   * position is not modified.\n   *\n   * @param buffer The {@link ByteBuffer} from which to read the syncframe.\n   * @return The number of audio samples represented by the syncframe.\n   */\n  public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) {\n    // See ETSI TS 102 366 subsection E.1.2.2.\n    int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;\n    return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6\n        : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);\n  }\n\n  /**\n   * Returns the offset relative to the buffer's position of the start of a TrueHD syncframe, or\n   * {@link C#INDEX_UNSET} if no syncframe was found. The buffer's position is not modified.\n   *\n   * @param buffer The {@link ByteBuffer} within which to find a syncframe.\n   * @return The offset relative to the buffer's position of the start of a TrueHD syncframe, or\n   *     {@link C#INDEX_UNSET} if no syncframe was found.\n   */\n  public static int findTrueHdSyncframeOffset(ByteBuffer buffer) {\n    int startIndex = buffer.position();\n    int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH;\n    for (int i = startIndex; i <= endIndex; i++) {\n      // The syncword ends 0xBA for TrueHD or 0xBB for MLP.\n      if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) {\n        return i - startIndex;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the\n   * buffer is not the start of a syncframe.\n   *\n   * @param syncframe The bytes from which to read the syncframe. Must be at least {@link\n   *     #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long.\n   * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't\n   *     contain the start of a syncframe.\n   */\n  public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) {\n    // TODO: Link to specification if available.\n    // The syncword ends 0xBA for TrueHD or 0xBB for MLP.\n    if (syncframe[4] != (byte) 0xF8\n        || syncframe[5] != (byte) 0x72\n        || syncframe[6] != (byte) 0x6F\n        || (syncframe[7] & 0xFE) != 0xBA) {\n      return 0;\n    }\n    boolean isMlp = (syncframe[7] & 0xFF) == 0xBB;\n    return 40 << ((syncframe[isMlp ? 9 : 8] >> 4) & 0x07);\n  }\n\n  /**\n   * Reads the number of audio samples represented by a TrueHD syncframe. The buffer's position is\n   * not modified.\n   *\n   * @param buffer The {@link ByteBuffer} from which to read the syncframe.\n   * @param offset The offset of the start of the syncframe relative to the buffer's position.\n   * @return The number of audio samples represented by the syncframe.\n   */\n  public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer, int offset) {\n    // TODO: Link to specification if available.\n    boolean isMlp = (buffer.get(buffer.position() + offset + 7) & 0xFF) == 0xBB;\n    return 40 << ((buffer.get(buffer.position() + offset + (isMlp ? 9 : 8)) >> 4) & 0x07);\n  }\n\n  private static int getAc3SyncframeSize(int fscod, int frmsizecod) {\n    int halfFrmsizecod = frmsizecod / 2;\n    if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0\n        || halfFrmsizecod >= SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1.length) {\n      // Invalid values provided.\n      return C.LENGTH_UNSET;\n    }\n    int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];\n    if (sampleRate == 44100) {\n      return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[halfFrmsizecod] + (frmsizecod % 2));\n    }\n    int bitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod];\n    if (sampleRate == 32000) {\n      return 6 * bitrate;\n    } else { // sampleRate == 48000\n      return 4 * bitrate;\n    }\n  }\n\n  private Ac3Util() {}\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\n\n/** Utility methods for parsing AC-4 frames, which are access units in AC-4 bitstreams. */\npublic final class Ac4Util {\n\n  /** Holds sample format information as presented by a syncframe header. */\n  public static final class SyncFrameInfo {\n\n    /** The bitstream version. */\n    public final int bitstreamVersion;\n    /** The audio sampling rate in Hz. */\n    public final int sampleRate;\n    /** The number of audio channels */\n    public final int channelCount;\n    /** The size of the frame. */\n    public final int frameSize;\n    /** Number of audio samples in the frame. */\n    public final int sampleCount;\n\n    private SyncFrameInfo(\n        int bitstreamVersion, int channelCount, int sampleRate, int frameSize, int sampleCount) {\n      this.bitstreamVersion = bitstreamVersion;\n      this.channelCount = channelCount;\n      this.sampleRate = sampleRate;\n      this.frameSize = frameSize;\n      this.sampleCount = sampleCount;\n    }\n  }\n\n  public static final int AC40_SYNCWORD = 0xAC40;\n  public static final int AC41_SYNCWORD = 0xAC41;\n\n  /** The channel count of AC-4 stream. */\n  // TODO: Parse AC-4 stream channel count.\n  private static final int CHANNEL_COUNT_2 = 2;\n  /**\n   * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full\n   * header size.\n   */\n  public static final int HEADER_SIZE_FOR_PARSER = 16;\n  /**\n   * Number of audio samples in the frame. Defined in IEC61937-14:2017 table 5 and 6. This table\n   * provides the number of samples per frame at the playback sampling frequency of 48 kHz. For 44.1\n   * kHz, only frame_rate_index(13) is valid and corresponding sample count is 2048.\n   */\n  private static final int[] SAMPLE_COUNT =\n      new int[] {\n        /* [ 0]  23.976 fps */ 2002,\n        /* [ 1]  24     fps */ 2000,\n        /* [ 2]  25     fps */ 1920,\n        /* [ 3]  29.97  fps */ 1601, // 1601 | 1602 | 1601 | 1602 | 1602\n        /* [ 4]  30     fps */ 1600,\n        /* [ 5]  47.95  fps */ 1001,\n        /* [ 6]  48     fps */ 1000,\n        /* [ 7]  50     fps */ 960,\n        /* [ 8]  59.94  fps */ 800, //  800 |  801 |  801 |  801 |  801\n        /* [ 9]  60     fps */ 800,\n        /* [10] 100     fps */ 480,\n        /* [11] 119.88  fps */ 400, //  400 |  400 |  401 |  400 |  401\n        /* [12] 120     fps */ 400,\n        /* [13]  23.438 fps */ 2048\n      };\n\n  /**\n   * Returns the AC-4 format given {@code data} containing the AC4SpecificBox according to ETSI TS\n   * 103 190-1 Annex E. The reading position of {@code data} will be modified.\n   *\n   * @param data The AC4SpecificBox to parse.\n   * @param trackId The track identifier to set on the format.\n   * @param language The language to set on the format.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @return The AC-4 format parsed from data in the header.\n   */\n  public static Format parseAc4AnnexEFormat(\n      ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) {\n    data.skipBytes(1); // ac4_dsi_version, bitstream_version[0:5]\n    int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100;\n    return Format.createAudioSampleFormat(\n        trackId,\n        MimeTypes.AUDIO_AC4,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* maxInputSize= */ Format.NO_VALUE,\n        CHANNEL_COUNT_2,\n        sampleRate,\n        /* initializationData= */ null,\n        drmInitData,\n        /* selectionFlags= */ 0,\n        language);\n  }\n\n  /**\n   * Returns AC-4 format information given {@code data} containing a syncframe. The reading position\n   * of {@code data} will be modified.\n   *\n   * @param data The data to parse, positioned at the start of the syncframe.\n   * @return The AC-4 format data parsed from the header.\n   */\n  public static SyncFrameInfo parseAc4SyncframeInfo(ParsableBitArray data) {\n    int headerSize = 0;\n    int syncWord = data.readBits(16);\n    headerSize += 2;\n    int frameSize = data.readBits(16);\n    headerSize += 2;\n    if (frameSize == 0xFFFF) {\n      frameSize = data.readBits(24);\n      headerSize += 3; // Extended frame_size\n    }\n    frameSize += headerSize;\n    if (syncWord == AC41_SYNCWORD) {\n      frameSize += 2; // crc_word\n    }\n    int bitstreamVersion = data.readBits(2);\n    if (bitstreamVersion == 3) {\n      bitstreamVersion += readVariableBits(data, /* bitsPerRead= */ 2);\n    }\n    int sequenceCounter = data.readBits(10);\n    if (data.readBit()) { // b_wait_frames\n      if (data.readBits(3) > 0) { // wait_frames\n        data.skipBits(2); // reserved\n      }\n    }\n    int sampleRate = data.readBit() ? 48000 : 44100;\n    int frameRateIndex = data.readBits(4);\n    int sampleCount = 0;\n    if (sampleRate == 44100 && frameRateIndex == 13) {\n      sampleCount = SAMPLE_COUNT[frameRateIndex];\n    } else if (sampleRate == 48000 && frameRateIndex < SAMPLE_COUNT.length) {\n      sampleCount = SAMPLE_COUNT[frameRateIndex];\n      switch (sequenceCounter % 5) {\n        case 1: // fall through\n        case 3:\n          if (frameRateIndex == 3 || frameRateIndex == 8) {\n            sampleCount++;\n          }\n          break;\n        case 2:\n          if (frameRateIndex == 8 || frameRateIndex == 11) {\n            sampleCount++;\n          }\n          break;\n        case 4:\n          if (frameRateIndex == 3 || frameRateIndex == 8 || frameRateIndex == 11) {\n            sampleCount++;\n          }\n          break;\n        default:\n          break;\n      }\n    }\n    return new SyncFrameInfo(bitstreamVersion, CHANNEL_COUNT_2, sampleRate, frameSize, sampleCount);\n  }\n\n  /**\n   * Returns the size in bytes of the given AC-4 syncframe.\n   *\n   * @param data The syncframe to parse.\n   * @param syncword The syncword value for the syncframe.\n   * @return The syncframe size in bytes, or {@link C#LENGTH_UNSET} if the input is invalid.\n   */\n  public static int parseAc4SyncframeSize(byte[] data, int syncword) {\n    if (data.length < 7) {\n      return C.LENGTH_UNSET;\n    }\n    int headerSize = 2; // syncword\n    int frameSize = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);\n    headerSize += 2;\n    if (frameSize == 0xFFFF) {\n      frameSize = ((data[4] & 0xFF) << 16) | ((data[5] & 0xFF) << 8) | (data[6] & 0xFF);\n      headerSize += 3;\n    }\n    if (syncword == AC41_SYNCWORD) {\n      headerSize += 2;\n    }\n    frameSize += headerSize;\n    return frameSize;\n  }\n\n  /**\n   * Reads the number of audio samples represented by the given AC-4 syncframe. The buffer's\n   * position is not modified.\n   *\n   * @param buffer The {@link ByteBuffer} from which to read the syncframe.\n   * @return The number of audio samples represented by the syncframe.\n   */\n  public static int parseAc4SyncframeAudioSampleCount(ByteBuffer buffer) {\n    byte[] bufferBytes = new byte[HEADER_SIZE_FOR_PARSER];\n    int position = buffer.position();\n    buffer.get(bufferBytes);\n    buffer.position(position);\n    return parseAc4SyncframeInfo(new ParsableBitArray(bufferBytes)).sampleCount;\n  }\n\n  /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */\n  public static void getAc4SampleHeader(int size, ParsableByteArray buffer) {\n    // See ETSI TS 103 190-1 V1.3.1, Annex G.\n    buffer.reset(/* limit= */ 7);\n    buffer.data[0] = (byte) 0xAC;\n    buffer.data[1] = 0x40;\n    buffer.data[2] = (byte) 0xFF;\n    buffer.data[3] = (byte) 0xFF;\n    buffer.data[4] = (byte) ((size >> 16) & 0xFF);\n    buffer.data[5] = (byte) ((size >> 8) & 0xFF);\n    buffer.data[6] = (byte) (size & 0xFF);\n  }\n\n  private static int readVariableBits(ParsableBitArray data, int bitsPerRead) {\n    int value = 0;\n    while (true) {\n      value += data.readBits(bitsPerRead);\n      if (!data.readBit()) {\n        break;\n      }\n      value++;\n      value <<= bitsPerRead;\n    }\n    return value;\n  }\n\n  private Ac4Util() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.annotation.TargetApi;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Attributes for audio playback, which configure the underlying platform\n * {@link android.media.AudioTrack}.\n * <p>\n * To set the audio attributes, create an instance using the {@link Builder} and either pass it to\n * {@link com.google.android.exoplayer2.SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or\n * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers.\n * <p>\n * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported\n * API versions.\n */\npublic final class AudioAttributes {\n\n  public static final AudioAttributes DEFAULT = new Builder().build();\n\n  /**\n   * Builder for {@link AudioAttributes}.\n   */\n  public static final class Builder {\n\n    private @C.AudioContentType int contentType;\n    private @C.AudioFlags int flags;\n    private @C.AudioUsage int usage;\n    private @C.AudioAllowedCapturePolicy int allowedCapturePolicy;\n\n    /**\n     * Creates a new builder for {@link AudioAttributes}.\n     *\n     * <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link\n     * C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.\n     */\n    public Builder() {\n      contentType = C.CONTENT_TYPE_UNKNOWN;\n      flags = 0;\n      usage = C.USAGE_MEDIA;\n      allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;\n    }\n\n    /**\n     * @see android.media.AudioAttributes.Builder#setContentType(int)\n     */\n    public Builder setContentType(@C.AudioContentType int contentType) {\n      this.contentType = contentType;\n      return this;\n    }\n\n    /**\n     * @see android.media.AudioAttributes.Builder#setFlags(int)\n     */\n    public Builder setFlags(@C.AudioFlags int flags) {\n      this.flags = flags;\n      return this;\n    }\n\n    /**\n     * @see android.media.AudioAttributes.Builder#setUsage(int)\n     */\n    public Builder setUsage(@C.AudioUsage int usage) {\n      this.usage = usage;\n      return this;\n    }\n\n    /** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */\n    public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {\n      this.allowedCapturePolicy = allowedCapturePolicy;\n      return this;\n    }\n\n    /** Creates an {@link AudioAttributes} instance from this builder. */\n    public AudioAttributes build() {\n      return new AudioAttributes(contentType, flags, usage, allowedCapturePolicy);\n    }\n\n  }\n\n  public final @C.AudioContentType int contentType;\n  public final @C.AudioFlags int flags;\n  public final @C.AudioUsage int usage;\n  public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy;\n\n  @Nullable private android.media.AudioAttributes audioAttributesV21;\n\n  private AudioAttributes(\n      @C.AudioContentType int contentType,\n      @C.AudioFlags int flags,\n      @C.AudioUsage int usage,\n      @C.AudioAllowedCapturePolicy int allowedCapturePolicy) {\n    this.contentType = contentType;\n    this.flags = flags;\n    this.usage = usage;\n    this.allowedCapturePolicy = allowedCapturePolicy;\n  }\n\n  /**\n   * Returns a {@link android.media.AudioAttributes} from this instance.\n   *\n   * <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29.\n   */\n  @TargetApi(21)\n  public android.media.AudioAttributes getAudioAttributesV21() {\n    if (audioAttributesV21 == null) {\n      android.media.AudioAttributes.Builder builder =\n          new android.media.AudioAttributes.Builder()\n              .setContentType(contentType)\n              .setFlags(flags)\n              .setUsage(usage);\n      if (Util.SDK_INT >= 29) {\n        builder.setAllowedCapturePolicy(allowedCapturePolicy);\n      }\n      audioAttributesV21 = builder.build();\n    }\n    return audioAttributesV21;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    AudioAttributes other = (AudioAttributes) obj;\n    return this.contentType == other.contentType\n        && this.flags == other.flags\n        && this.usage == other.usage\n        && this.allowedCapturePolicy == other.allowedCapturePolicy;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + contentType;\n    result = 31 * result + flags;\n    result = 31 * result + usage;\n    result = 31 * result + allowedCapturePolicy;\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.media.AudioFormat;\nimport android.media.AudioManager;\nimport android.net.Uri;\nimport android.provider.Settings.Global;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/** Represents the set of audio formats that a device is capable of playing. */\n@TargetApi(21)\npublic final class AudioCapabilities {\n\n  private static final int DEFAULT_MAX_CHANNEL_COUNT = 8;\n\n  /** The minimum audio capabilities supported by all devices. */\n  public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =\n      new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, DEFAULT_MAX_CHANNEL_COUNT);\n\n  /** Audio capabilities when the device specifies external surround sound. */\n  private static final AudioCapabilities EXTERNAL_SURROUND_SOUND_CAPABILITIES =\n      new AudioCapabilities(\n          new int[] {\n            AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_AC3, AudioFormat.ENCODING_E_AC3\n          },\n          DEFAULT_MAX_CHANNEL_COUNT);\n\n  /** Global settings key for devices that can specify external surround sound. */\n  private static final String EXTERNAL_SURROUND_SOUND_KEY = \"external_surround_sound_enabled\";\n\n  /**\n   * Returns the current audio capabilities for the device.\n   *\n   * @param context A context for obtaining the current audio capabilities.\n   * @return The current audio capabilities for the device.\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  public static AudioCapabilities getCapabilities(Context context) {\n    Intent intent =\n        context.registerReceiver(\n            /* receiver= */ null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));\n    return getCapabilities(context, intent);\n  }\n\n  @SuppressLint(\"InlinedApi\")\n  /* package */ static AudioCapabilities getCapabilities(Context context, @Nullable Intent intent) {\n    if (deviceMaySetExternalSurroundSoundGlobalSetting()\n        && Global.getInt(context.getContentResolver(), EXTERNAL_SURROUND_SOUND_KEY, 0) == 1) {\n      return EXTERNAL_SURROUND_SOUND_CAPABILITIES;\n    }\n    if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {\n      return DEFAULT_AUDIO_CAPABILITIES;\n    }\n    return new AudioCapabilities(\n        intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),\n        intent.getIntExtra(\n            AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));\n  }\n\n  /**\n   * Returns the global settings {@link Uri} used by the device to specify external surround sound,\n   * or null if the device does not support this functionality.\n   */\n  @Nullable\n  /* package */ static Uri getExternalSurroundSoundGlobalSettingUri() {\n    return deviceMaySetExternalSurroundSoundGlobalSetting()\n        ? Global.getUriFor(EXTERNAL_SURROUND_SOUND_KEY)\n        : null;\n  }\n\n  private final int[] supportedEncodings;\n  private final int maxChannelCount;\n\n  /**\n   * Constructs new audio capabilities based on a set of supported encodings and a maximum channel\n   * count.\n   *\n   * <p>Applications should generally call {@link #getCapabilities(Context)} to obtain an instance\n   * based on the capabilities advertised by the platform, rather than calling this constructor.\n   *\n   * @param supportedEncodings Supported audio encodings from {@link AudioFormat}'s\n   *     {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are\n   *     supported.\n   * @param maxChannelCount The maximum number of audio channels that can be played simultaneously.\n   */\n  public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {\n    if (supportedEncodings != null) {\n      this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);\n      Arrays.sort(this.supportedEncodings);\n    } else {\n      this.supportedEncodings = new int[0];\n    }\n    this.maxChannelCount = maxChannelCount;\n  }\n\n  /**\n   * Returns whether this device supports playback of the specified audio {@code encoding}.\n   *\n   * @param encoding One of {@link AudioFormat}'s {@code ENCODING_*} constants.\n   * @return Whether this device supports playback the specified audio {@code encoding}.\n   */\n  public boolean supportsEncoding(int encoding) {\n    return Arrays.binarySearch(supportedEncodings, encoding) >= 0;\n  }\n\n  /**\n   * Returns the maximum number of channels the device can play at the same time.\n   */\n  public int getMaxChannelCount() {\n    return maxChannelCount;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object other) {\n    if (this == other) {\n      return true;\n    }\n    if (!(other instanceof AudioCapabilities)) {\n      return false;\n    }\n    AudioCapabilities audioCapabilities = (AudioCapabilities) other;\n    return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)\n        && maxChannelCount == audioCapabilities.maxChannelCount;\n  }\n\n  @Override\n  public int hashCode() {\n    return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);\n  }\n\n  @Override\n  public String toString() {\n    return \"AudioCapabilities[maxChannelCount=\" + maxChannelCount\n        + \", supportedEncodings=\" + Arrays.toString(supportedEncodings) + \"]\";\n  }\n\n  private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() {\n    return Util.SDK_INT >= 17 && \"Amazon\".equals(Util.MANUFACTURER);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.content.BroadcastReceiver;\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.database.ContentObserver;\nimport android.media.AudioManager;\nimport android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Receives broadcast events indicating changes to the device's audio capabilities, notifying a\n * {@link Listener} when audio capability changes occur.\n */\npublic final class AudioCapabilitiesReceiver {\n\n  /**\n   * Listener notified when audio capabilities change.\n   */\n  public interface Listener {\n\n    /**\n     * Called when the audio capabilities change.\n     *\n     * @param audioCapabilities The current audio capabilities for the device.\n     */\n    void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);\n\n  }\n\n  private final Context context;\n  private final Listener listener;\n  private final Handler handler;\n  @Nullable private final BroadcastReceiver receiver;\n  @Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver;\n\n  /* package */ @Nullable AudioCapabilities audioCapabilities;\n  private boolean registered;\n\n  /**\n   * @param context A context for registering the receiver.\n   * @param listener The listener to notify when audio capabilities change.\n   */\n  public AudioCapabilitiesReceiver(Context context, Listener listener) {\n    context = context.getApplicationContext();\n    this.context = context;\n    this.listener = Assertions.checkNotNull(listener);\n    handler = new Handler(Util.getLooper());\n    receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;\n    Uri externalSurroundSoundUri = AudioCapabilities.getExternalSurroundSoundGlobalSettingUri();\n    externalSurroundSoundSettingObserver =\n        externalSurroundSoundUri != null\n            ? new ExternalSurroundSoundSettingObserver(\n                handler, context.getContentResolver(), externalSurroundSoundUri)\n            : null;\n  }\n\n  /**\n   * Registers the receiver, meaning it will notify the listener when audio capability changes\n   * occur. The current audio capabilities will be returned. It is important to call\n   * {@link #unregister} when the receiver is no longer required.\n   *\n   * @return The current audio capabilities for the device.\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  public AudioCapabilities register() {\n    if (registered) {\n      return Assertions.checkNotNull(audioCapabilities);\n    }\n    registered = true;\n    if (externalSurroundSoundSettingObserver != null) {\n      externalSurroundSoundSettingObserver.register();\n    }\n    Intent stickyIntent = null;\n    if (receiver != null) {\n      IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG);\n      stickyIntent =\n          context.registerReceiver(\n              receiver, intentFilter, /* broadcastPermission= */ null, handler);\n    }\n    audioCapabilities = AudioCapabilities.getCapabilities(context, stickyIntent);\n    return audioCapabilities;\n  }\n\n  /**\n   * Unregisters the receiver, meaning it will no longer notify the listener when audio capability\n   * changes occur.\n   */\n  public void unregister() {\n    if (!registered) {\n      return;\n    }\n    audioCapabilities = null;\n    if (receiver != null) {\n      context.unregisterReceiver(receiver);\n    }\n    if (externalSurroundSoundSettingObserver != null) {\n      externalSurroundSoundSettingObserver.unregister();\n    }\n    registered = false;\n  }\n\n  private void onNewAudioCapabilities(AudioCapabilities newAudioCapabilities) {\n    if (registered && !newAudioCapabilities.equals(audioCapabilities)) {\n      audioCapabilities = newAudioCapabilities;\n      listener.onAudioCapabilitiesChanged(newAudioCapabilities);\n    }\n  }\n\n  private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n      if (!isInitialStickyBroadcast()) {\n        onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, intent));\n      }\n    }\n  }\n\n  private final class ExternalSurroundSoundSettingObserver extends ContentObserver {\n\n    private final ContentResolver resolver;\n    private final Uri settingUri;\n\n    public ExternalSurroundSoundSettingObserver(\n        Handler handler, ContentResolver resolver, Uri settingUri) {\n      super(handler);\n      this.resolver = resolver;\n      this.settingUri = settingUri;\n    }\n\n    public void register() {\n      resolver.registerContentObserver(settingUri, /* notifyForDescendants= */ false, this);\n    }\n\n    public void unregister() {\n      resolver.unregisterContentObserver(this);\n    }\n\n    @Override\n    public void onChange(boolean selfChange) {\n      onNewAudioCapabilities(AudioCapabilities.getCapabilities(context));\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\n/** Thrown when an audio decoder error occurs. */\npublic class AudioDecoderException extends Exception {\n\n  /** @param message The detail message for this exception. */\n  public AudioDecoderException(String message) {\n    super(message);\n  }\n\n  /**\n   * @param message The detail message for this exception.\n   * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).\n   *     A <tt>null</tt> value is permitted, and indicates that the cause is nonexistent or unknown.\n   */\n  public AudioDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\n/** A listener for changes in audio configuration. */\npublic interface AudioListener {\n\n  /**\n   * Called when the audio session is set.\n   *\n   * @param audioSessionId The audio session id.\n   */\n  default void onAudioSessionId(int audioSessionId) {}\n\n  /**\n   * Called when the audio attributes change.\n   *\n   * @param audioAttributes The audio attributes.\n   */\n  default void onAudioAttributesChanged(AudioAttributes audioAttributes) {}\n\n  /**\n   * Called when the volume changes.\n   *\n   * @param volume The new volume, with 0 being silence and 1 being unity gain.\n   */\n  default void onVolumeChanged(float volume) {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * Interface for audio processors, which take audio data as input and transform it, potentially\n * modifying its channel count, encoding and/or sample rate.\n *\n * <p>In addition to being able to modify the format of audio, implementations may allow parameters\n * to be set that affect the output audio and whether the processor is active/inactive.\n */\npublic interface AudioProcessor {\n\n  /** PCM audio format that may be handled by an audio processor. */\n  final class AudioFormat {\n    public static final AudioFormat NOT_SET =\n        new AudioFormat(\n            /* sampleRate= */ Format.NO_VALUE,\n            /* channelCount= */ Format.NO_VALUE,\n            /* encoding= */ Format.NO_VALUE);\n\n    /** The sample rate in Hertz. */\n    public final int sampleRate;\n    /** The number of interleaved channels. */\n    public final int channelCount;\n    /** The type of linear PCM encoding. */\n    @C.PcmEncoding public final int encoding;\n    /** The number of bytes used to represent one audio frame. */\n    public final int bytesPerFrame;\n\n    public AudioFormat(int sampleRate, int channelCount, @C.PcmEncoding int encoding) {\n      this.sampleRate = sampleRate;\n      this.channelCount = channelCount;\n      this.encoding = encoding;\n      bytesPerFrame =\n          Util.isEncodingLinearPcm(encoding)\n              ? Util.getPcmFrameSize(encoding, channelCount)\n              : Format.NO_VALUE;\n    }\n\n    @Override\n    public String toString() {\n      return \"AudioFormat[\"\n          + \"sampleRate=\"\n          + sampleRate\n          + \", channelCount=\"\n          + channelCount\n          + \", encoding=\"\n          + encoding\n          + ']';\n    }\n  }\n\n  /** Exception thrown when a processor can't be configured for a given input audio format. */\n  final class UnhandledAudioFormatException extends Exception {\n\n    public UnhandledAudioFormatException(AudioFormat inputAudioFormat) {\n      super(\"Unhandled format: \" + inputAudioFormat);\n    }\n\n  }\n\n  /** An empty, direct {@link ByteBuffer}. */\n  ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());\n\n  /**\n   * Configures the processor to process input audio with the specified format. After calling this\n   * method, call {@link #isActive()} to determine whether the audio processor is active. Returns\n   * the configured output audio format if this instance is active.\n   *\n   * <p>After calling this method, it is necessary to {@link #flush()} the processor to apply the\n   * new configuration. Before applying the new configuration, it is safe to queue input and get\n   * output in the old input/output formats. Call {@link #queueEndOfStream()} when no more input\n   * will be supplied in the old input format.\n   *\n   * @param inputAudioFormat The format of audio that will be queued after the next call to {@link\n   *     #flush()}.\n   * @return The configured output audio format if this instance is {@link #isActive() active}.\n   * @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input.\n   */\n  AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;\n\n  /** Returns whether the processor is configured and will process input buffers. */\n  boolean isActive();\n\n  /**\n   * Queues audio data between the position and limit of the input {@code buffer} for processing.\n   * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as\n   * read-only. Its position will be advanced by the number of bytes consumed (which may be zero).\n   * The caller retains ownership of the provided buffer. Calling this method invalidates any\n   * previous buffer returned by {@link #getOutput()}.\n   *\n   * @param buffer The input buffer to process.\n   */\n  void queueInput(ByteBuffer buffer);\n\n  /**\n   * Queues an end of stream signal. After this method has been called,\n   * {@link #queueInput(ByteBuffer)} may not be called until after the next call to\n   * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple\n   * calls may be required to read all of the remaining output data. {@link #isEnded()} will return\n   * {@code true} once all remaining output data has been read.\n   */\n  void queueEndOfStream();\n\n  /**\n   * Returns a buffer containing processed output data between its position and limit. The buffer\n   * will always be a direct byte buffer with native byte order. Calling this method invalidates any\n   * previously returned buffer. The buffer will be empty if no output is available.\n   *\n   * @return A buffer containing processed output data between its position and limit.\n   */\n  ByteBuffer getOutput();\n\n  /**\n   * Returns whether this processor will return no more output from {@link #getOutput()} until it\n   * has been {@link #flush()}ed and more input has been queued.\n   */\n  boolean isEnded();\n\n  /**\n   * Clears any buffered data and pending output. If the audio processor is active, also prepares\n   * the audio processor to receive a new stream of input in the last configured (pending) format.\n   */\n  void flush();\n\n  /** Resets the processor to its unconfigured state, releasing any resources. */\n  void reset();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Listener of audio {@link Renderer} events. All methods have no-op default implementations to\n * allow selective overrides.\n */\npublic interface AudioRendererEventListener {\n\n  /**\n   * Called when the renderer is enabled.\n   *\n   * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it\n   *     remains enabled.\n   */\n  default void onAudioEnabled(DecoderCounters counters) {}\n\n  /**\n   * Called when the audio session is set.\n   *\n   * @param audioSessionId The audio session id.\n   */\n  default void onAudioSessionId(int audioSessionId) {}\n\n  /**\n   * Called when a decoder is created.\n   *\n   * @param decoderName The decoder that was created.\n   * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization\n   *     finished.\n   * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.\n   */\n  default void onAudioDecoderInitialized(\n          String decoderName, long initializedTimestampMs, long initializationDurationMs) {}\n\n  /**\n   * Called when the format of the media being consumed by the renderer changes.\n   *\n   * @param format The new format.\n   */\n  default void onAudioInputFormatChanged(Format format) {}\n\n  /**\n   * Called when an {@link AudioSink} underrun occurs.\n   *\n   * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes.\n   * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is\n   *     configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,\n   *     as the buffered media can have a variable bitrate so the duration may be unknown.\n   * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.\n   */\n  default void onAudioSinkUnderrun(\n          int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}\n\n  /**\n   * Called when the renderer is disabled.\n   *\n   * @param counters {@link DecoderCounters} that were updated by the renderer.\n   */\n  default void onAudioDisabled(DecoderCounters counters) {}\n\n  /**\n   * Dispatches events to a {@link AudioRendererEventListener}.\n   */\n  final class EventDispatcher {\n\n    @Nullable private final Handler handler;\n    @Nullable private final AudioRendererEventListener listener;\n\n    /**\n     * @param handler A handler for dispatching events, or null if creating a dummy instance.\n     * @param listener The listener to which events should be dispatched, or null if creating a\n     *     dummy instance.\n     */\n    public EventDispatcher(@Nullable Handler handler,\n        @Nullable AudioRendererEventListener listener) {\n      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;\n      this.listener = listener;\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.\n     */\n    public void enabled(final DecoderCounters decoderCounters) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters));\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.\n     */\n    public void decoderInitialized(final String decoderName,\n        final long initializedTimestampMs, final long initializationDurationMs) {\n      if (handler != null) {\n        handler.post(\n            () ->\n                castNonNull(listener)\n                    .onAudioDecoderInitialized(\n                        decoderName, initializedTimestampMs, initializationDurationMs));\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.\n     */\n    public void inputFormatChanged(final Format format) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format));\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioSinkUnderrun(int, long, long)}.\n     */\n    public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs,\n        final long elapsedSinceLastFeedMs) {\n      if (handler != null) {\n        handler.post(\n            () ->\n                castNonNull(listener)\n                    .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs));\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.\n     */\n    public void disabled(final DecoderCounters counters) {\n      counters.ensureUpdated();\n      if (handler != null) {\n        handler.post(\n            () -> {\n              counters.ensureUpdated();\n              castNonNull(listener).onAudioDisabled(counters);\n            });\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.\n     */\n    public void audioSessionId(final int audioSessionId) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.media.AudioTrack;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport java.nio.ByteBuffer;\n\n/**\n * A sink that consumes audio data.\n *\n * <p>Before starting playback, specify the input audio format by calling {@link #configure(int,\n * int, int, int, int[], int, int)}.\n *\n * <p>Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}\n * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.\n *\n * <p>Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format\n * changes. The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer,\n * long)}.\n *\n * <p>Call {@link #flush()} to prepare the sink to receive audio data from a new playback position.\n *\n * <p>Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers\n * will be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #flush()}.\n * Call {@link #reset()} when the instance is no longer required.\n *\n * <p>The implementation may be backed by a platform {@link AudioTrack}. In this case, {@link\n * #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, {@link\n * #enableTunnelingV21(int)} and/or {@link #disableTunneling()} may be called before writing data to\n * the sink. These methods may also be called after writing data to the sink, in which case it will\n * be reinitialized as required. For implementations that are not based on platform {@link\n * AudioTrack}s, calling methods relating to audio sessions, audio attributes, and tunneling may\n * have no effect.\n */\npublic interface AudioSink {\n\n  /**\n   * Listener for audio sink events.\n   */\n  interface Listener {\n\n    /**\n     * Called if the audio sink has started rendering audio to a new platform audio session.\n     *\n     * @param audioSessionId The newly generated audio session's identifier.\n     */\n    void onAudioSessionId(int audioSessionId);\n\n    /**\n     * Called when the audio sink handles a buffer whose timestamp is discontinuous with the last\n     * buffer handled since it was reset.\n     */\n    void onPositionDiscontinuity();\n\n    /**\n     * Called when the audio sink runs out of data.\n     * <p>\n     * An audio sink implementation may never call this method (for example, if audio data is\n     * consumed in batches rather than based on the sink's own clock).\n     *\n     * @param bufferSize The size of the sink's buffer, in bytes.\n     * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for\n     *     PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the\n     *     buffered media can have a variable bitrate so the duration may be unknown.\n     * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds.\n     */\n    void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);\n\n  }\n\n  /**\n   * Thrown when a failure occurs configuring the sink.\n   */\n  final class ConfigurationException extends Exception {\n\n    /**\n     * Creates a new configuration exception with the specified {@code cause} and no message.\n     */\n    public ConfigurationException(Throwable cause) {\n      super(cause);\n    }\n\n    /**\n     * Creates a new configuration exception with the specified {@code message} and no cause.\n     */\n    public ConfigurationException(String message) {\n      super(message);\n    }\n\n  }\n\n  /**\n   * Thrown when a failure occurs initializing the sink.\n   */\n  final class InitializationException extends Exception {\n\n    /**\n     * The underlying {@link AudioTrack}'s state, if applicable.\n     */\n    public final int audioTrackState;\n\n    /**\n     * @param audioTrackState The underlying {@link AudioTrack}'s state, if applicable.\n     * @param sampleRate The requested sample rate in Hz.\n     * @param channelConfig The requested channel configuration.\n     * @param bufferSize The requested buffer size in bytes.\n     */\n    public InitializationException(int audioTrackState, int sampleRate, int channelConfig,\n        int bufferSize) {\n      super(\"AudioTrack init failed: \" + audioTrackState + \", Config(\" + sampleRate + \", \"\n          + channelConfig + \", \" + bufferSize + \")\");\n      this.audioTrackState = audioTrackState;\n    }\n\n  }\n\n  /**\n   * Thrown when a failure occurs writing to the sink.\n   */\n  final class WriteException extends Exception {\n\n    /**\n     * The error value returned from the sink implementation. If the sink writes to a platform\n     * {@link AudioTrack}, this will be the error value returned from\n     * {@link AudioTrack#write(byte[], int, int)} or {@link AudioTrack#write(ByteBuffer, int, int)}.\n     * Otherwise, the meaning of the error code depends on the sink implementation.\n     */\n    public final int errorCode;\n\n    /**\n     * @param errorCode The error value returned from the sink implementation.\n     */\n    public WriteException(int errorCode) {\n      super(\"AudioTrack write failed: \" + errorCode);\n      this.errorCode = errorCode;\n    }\n\n  }\n\n  /**\n   * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set.\n   */\n  long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;\n\n  /**\n   * Sets the listener for sink events, which should be the audio renderer.\n   *\n   * @param listener The listener for sink events, which should be the audio renderer.\n   */\n  void setListener(Listener listener);\n\n  /**\n   * Returns whether the sink supports the audio format.\n   *\n   * @param channelCount The number of channels, or {@link Format#NO_VALUE} if not known.\n   * @param encoding The audio encoding, or {@link Format#NO_VALUE} if not known.\n   * @return Whether the sink supports the audio format.\n   */\n  boolean supportsOutput(int channelCount, @C.Encoding int encoding);\n\n  /**\n   * Returns the playback position in the stream starting at zero, in microseconds, or\n   * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.\n   *\n   * @param sourceEnded Specify {@code true} if no more input buffers will be provided.\n   * @return The playback position relative to the start of playback, in microseconds.\n   */\n  long getCurrentPositionUs(boolean sourceEnded);\n\n  /**\n   * Configures (or reconfigures) the sink.\n   *\n   * @param inputEncoding The encoding of audio data provided in the input buffers.\n   * @param inputChannelCount The number of channels.\n   * @param inputSampleRate The sample rate in Hz.\n   * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a\n   *     suitable buffer size.\n   * @param outputChannels A mapping from input to output channels that is applied to this sink's\n   *     input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the\n   *     input unchanged. Otherwise, the element at index {@code i} specifies index of the input\n   *     channel to map to output channel {@code i} when preprocessing input buffers. After the map\n   *     is applied the audio data will have {@code outputChannels.length} channels.\n   * @param trimStartFrames The number of audio frames to trim from the start of data written to the\n   *     sink after this call.\n   * @param trimEndFrames The number of audio frames to trim from data written to the sink\n   *     immediately preceding the next call to {@link #flush()} or this method.\n   * @throws ConfigurationException If an error occurs configuring the sink.\n   */\n  void configure(\n          @C.Encoding int inputEncoding,\n          int inputChannelCount,\n          int inputSampleRate,\n          int specifiedBufferSize,\n          @Nullable int[] outputChannels,\n          int trimStartFrames,\n          int trimEndFrames)\n      throws ConfigurationException;\n\n  /**\n   * Starts or resumes consuming audio if initialized.\n   */\n  void play();\n\n  /** Signals to the sink that the next buffer may be discontinuous with the previous buffer. */\n  void handleDiscontinuity();\n\n  /**\n   * Attempts to process data from a {@link ByteBuffer}, starting from its current position and\n   * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the\n   * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if\n   * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset.\n   *\n   * <p>Returns whether the data was handled in full. If the data was not handled in full then the\n   * same {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,\n   * except in the case of an intervening call to {@link #flush()} (or to {@link #configure(int,\n   * int, int, int, int[], int, int)} that causes the sink to be flushed).\n   *\n   * @param buffer The buffer containing audio data.\n   * @param presentationTimeUs The presentation timestamp of the buffer in microseconds.\n   * @return Whether the buffer was handled fully.\n   * @throws InitializationException If an error occurs initializing the sink.\n   * @throws WriteException If an error occurs writing the audio data.\n   */\n  boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)\n      throws InitializationException, WriteException;\n\n  /**\n   * Processes any remaining data. {@link #isEnded()} will return {@code true} when no data remains.\n   *\n   * @throws WriteException If an error occurs draining data to the sink.\n   */\n  void playToEndOfStream() throws WriteException;\n\n  /**\n   * Returns whether {@link #playToEndOfStream} has been called and all buffers have been processed.\n   */\n  boolean isEnded();\n\n  /**\n   * Returns whether the sink has data pending that has not been consumed yet.\n   */\n  boolean hasPendingData();\n\n  /**\n   * Attempts to set the playback parameters. The audio sink may override these parameters if they\n   * are not supported.\n   *\n   * @param playbackParameters The new playback parameters to attempt to set.\n   */\n  void setPlaybackParameters(PlaybackParameters playbackParameters);\n\n  /**\n   * Gets the active {@link PlaybackParameters}.\n   */\n  PlaybackParameters getPlaybackParameters();\n\n  /**\n   * Sets attributes for audio playback. If the attributes have changed and if the sink is not\n   * configured for use with tunneling, then it is reset and the audio session id is cleared.\n   * <p>\n   * If the sink is configured for use with tunneling then the audio attributes are ignored. The\n   * sink is not reset and the audio session id is not cleared. The passed attributes will be used\n   * if the sink is later re-configured into non-tunneled mode.\n   *\n   * @param audioAttributes The attributes for audio playback.\n   */\n  void setAudioAttributes(AudioAttributes audioAttributes);\n\n  /** Sets the audio session id. */\n  void setAudioSessionId(int audioSessionId);\n\n  /** Sets the auxiliary effect. */\n  void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);\n\n  /**\n   * Enables tunneling, if possible. The sink is reset if tunneling was previously disabled or if\n   * the audio session id has changed. Enabling tunneling is only possible if the sink is based on a\n   * platform {@link AudioTrack}, and requires platform API version 21 onwards.\n   *\n   * @param tunnelingAudioSessionId The audio session id to use.\n   * @throws IllegalStateException Thrown if enabling tunneling on platform API version &lt; 21.\n   */\n  void enableTunnelingV21(int tunnelingAudioSessionId);\n\n  /**\n   * Disables tunneling. If tunneling was previously enabled then the sink is reset and any audio\n   * session id is cleared.\n   */\n  void disableTunneling();\n\n  /**\n   * Sets the playback volume.\n   *\n   * @param volume A volume in the range [0.0, 1.0].\n   */\n  void setVolume(float volume);\n\n  /**\n   * Pauses playback.\n   */\n  void pause();\n\n  /**\n   * Flushes the sink, after which it is ready to receive buffers from a new playback position.\n   *\n   * <p>The audio session may remain active until {@link #reset()} is called.\n   */\n  void flush();\n\n  /** Resets the renderer, releasing any resources that it currently holds. */\n  void reset();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.annotation.TargetApi;\nimport android.media.AudioTimestamp;\nimport android.media.AudioTrack;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at\n * the appropriate rate to detect when the timestamp starts to advance.\n *\n * <p>When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check\n * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and\n * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link\n * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it.\n *\n * <p>If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to\n * get the system time at which the latest timestamp was sampled and {@link\n * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()}\n * returns {@code true}, the caller should assume that the timestamp has been increasing in real\n * time since it was sampled. Otherwise, it may be stationary.\n *\n * <p>Call {@link #reset()} when pausing or resuming the track.\n */\n/* package */ final class AudioTimestampPoller {\n\n  /** Timestamp polling states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    STATE_INITIALIZING,\n    STATE_TIMESTAMP,\n    STATE_TIMESTAMP_ADVANCING,\n    STATE_NO_TIMESTAMP,\n    STATE_ERROR\n  })\n  private @interface State {}\n  /** State when first initializing. */\n  private static final int STATE_INITIALIZING = 0;\n  /** State when we have a timestamp and we don't know if it's advancing. */\n  private static final int STATE_TIMESTAMP = 1;\n  /** State when we have a timestamp and we know it is advancing. */\n  private static final int STATE_TIMESTAMP_ADVANCING = 2;\n  /** State when the no timestamp is available. */\n  private static final int STATE_NO_TIMESTAMP = 3;\n  /** State when the last timestamp was rejected as invalid. */\n  private static final int STATE_ERROR = 4;\n\n  /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */\n  private static final int FAST_POLL_INTERVAL_US = 5_000;\n  /**\n   * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}.\n   */\n  private static final int SLOW_POLL_INTERVAL_US = 10_000_000;\n  /** The polling interval for {@link #STATE_ERROR}. */\n  private static final int ERROR_POLL_INTERVAL_US = 500_000;\n\n  /**\n   * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being\n   * returned before transitioning to {@link #STATE_NO_TIMESTAMP}.\n   */\n  private static final int INITIALIZING_DURATION_US = 500_000;\n\n  @Nullable private final AudioTimestampV19 audioTimestamp;\n\n  private @State int state;\n  private long initializeSystemTimeUs;\n  private long sampleIntervalUs;\n  private long lastTimestampSampleTimeUs;\n  private long initialTimestampPositionFrames;\n\n  /**\n   * Creates a new audio timestamp poller.\n   *\n   * @param audioTrack The audio track that will provide timestamps, if the platform supports it.\n   */\n  public AudioTimestampPoller(AudioTrack audioTrack) {\n    if (Util.SDK_INT >= 19) {\n      audioTimestamp = new AudioTimestampV19(audioTrack);\n      reset();\n    } else {\n      audioTimestamp = null;\n      updateState(STATE_NO_TIMESTAMP);\n    }\n  }\n\n  /**\n   * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest\n   * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link\n   * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the\n   * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link\n   * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated.\n   *\n   * @param systemTimeUs The current system time, in microseconds.\n   * @return Whether the timestamp was updated.\n   */\n  public boolean maybePollTimestamp(long systemTimeUs) {\n    if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) {\n      return false;\n    }\n    lastTimestampSampleTimeUs = systemTimeUs;\n    boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp();\n    switch (state) {\n      case STATE_INITIALIZING:\n        if (updatedTimestamp) {\n          if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) {\n            // We have an initial timestamp, but don't know if it's advancing yet.\n            initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames();\n            updateState(STATE_TIMESTAMP);\n          } else {\n            // Drop the timestamp, as it was sampled before the last reset.\n            updatedTimestamp = false;\n          }\n        } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) {\n          // We haven't received a timestamp for a while, so they probably aren't available for the\n          // current audio route. Poll infrequently in case the route changes later.\n          // TODO: Ideally we should listen for audio route changes in order to detect when a\n          // timestamp becomes available again.\n          updateState(STATE_NO_TIMESTAMP);\n        }\n        break;\n      case STATE_TIMESTAMP:\n        if (updatedTimestamp) {\n          long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames();\n          if (timestampPositionFrames > initialTimestampPositionFrames) {\n            updateState(STATE_TIMESTAMP_ADVANCING);\n          }\n        } else {\n          reset();\n        }\n        break;\n      case STATE_TIMESTAMP_ADVANCING:\n        if (!updatedTimestamp) {\n          // The audio route may have changed, so reset polling.\n          reset();\n        }\n        break;\n      case STATE_NO_TIMESTAMP:\n        if (updatedTimestamp) {\n          // The audio route may have changed, so reset polling.\n          reset();\n        }\n        break;\n      case STATE_ERROR:\n        // Do nothing. If the caller accepts any new timestamp we'll reset polling.\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n    return updatedTimestamp;\n  }\n\n  /**\n   * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter\n   * the error state and poll timestamps infrequently until the next call to {@link\n   * #acceptTimestamp()}.\n   */\n  public void rejectTimestamp() {\n    updateState(STATE_ERROR);\n  }\n\n  /**\n   * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in\n   * the error state, it will begin to poll timestamps frequently again.\n   */\n  public void acceptTimestamp() {\n    if (state == STATE_ERROR) {\n      reset();\n    }\n  }\n\n  /**\n   * Returns whether this instance has a timestamp that can be used to calculate the audio track\n   * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link\n   * #getTimestampSystemTimeUs()} to access the timestamp.\n   */\n  public boolean hasTimestamp() {\n    return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING;\n  }\n\n  /**\n   * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link\n   * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A\n   * current position for the track can be extrapolated based on elapsed real time since the system\n   * time at which the timestamp was sampled.\n   */\n  public boolean isTimestampAdvancing() {\n    return state == STATE_TIMESTAMP_ADVANCING;\n  }\n\n  /** Resets polling. Should be called whenever the audio track is paused or resumed. */\n  public void reset() {\n    if (audioTimestamp != null) {\n      updateState(STATE_INITIALIZING);\n    }\n  }\n\n  /**\n   * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns\n   * the system time at which the latest timestamp was sampled, in microseconds.\n   */\n  public long getTimestampSystemTimeUs() {\n    return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET;\n  }\n\n  /**\n   * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns\n   * the latest timestamp's position in frames.\n   */\n  public long getTimestampPositionFrames() {\n    return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET;\n  }\n\n  private void updateState(@State int state) {\n    this.state = state;\n    switch (state) {\n      case STATE_INITIALIZING:\n        // Force polling a timestamp immediately, and poll quickly.\n        lastTimestampSampleTimeUs = 0;\n        initialTimestampPositionFrames = C.POSITION_UNSET;\n        initializeSystemTimeUs = System.nanoTime() / 1000;\n        sampleIntervalUs = FAST_POLL_INTERVAL_US;\n        break;\n      case STATE_TIMESTAMP:\n        sampleIntervalUs = FAST_POLL_INTERVAL_US;\n        break;\n      case STATE_TIMESTAMP_ADVANCING:\n      case STATE_NO_TIMESTAMP:\n        sampleIntervalUs = SLOW_POLL_INTERVAL_US;\n        break;\n      case STATE_ERROR:\n        sampleIntervalUs = ERROR_POLL_INTERVAL_US;\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  @TargetApi(19)\n  private static final class AudioTimestampV19 {\n\n    private final AudioTrack audioTrack;\n    private final AudioTimestamp audioTimestamp;\n\n    private long rawTimestampFramePositionWrapCount;\n    private long lastTimestampRawPositionFrames;\n    private long lastTimestampPositionFrames;\n\n    /**\n     * Creates a new {@link AudioTimestamp} wrapper.\n     *\n     * @param audioTrack The audio track that will provide timestamps.\n     */\n    public AudioTimestampV19(AudioTrack audioTrack) {\n      this.audioTrack = audioTrack;\n      audioTimestamp = new AudioTimestamp();\n    }\n\n    /**\n     * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was\n     * updated, in which case the updated timestamp system time and position can be accessed with\n     * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code\n     * false} if no timestamp is available, in which case those methods should not be called.\n     */\n    public boolean maybeUpdateTimestamp() {\n      boolean updated = audioTrack.getTimestamp(audioTimestamp);\n      if (updated) {\n        long rawPositionFrames = audioTimestamp.framePosition;\n        if (lastTimestampRawPositionFrames > rawPositionFrames) {\n          // The value must have wrapped around.\n          rawTimestampFramePositionWrapCount++;\n        }\n        lastTimestampRawPositionFrames = rawPositionFrames;\n        lastTimestampPositionFrames =\n            rawPositionFrames + (rawTimestampFramePositionWrapCount << 32);\n      }\n      return updated;\n    }\n\n    public long getTimestampSystemTimeUs() {\n      return audioTimestamp.nanoTime / 1000;\n    }\n\n    public long getTimestampPositionFrames() {\n      return lastTimestampPositionFrames;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.media.AudioTimestamp;\nimport android.media.AudioTrack;\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.reflect.Method;\n\n/**\n * Wraps an {@link AudioTrack}, exposing a position based on {@link\n * AudioTrack#getPlaybackHeadPosition()} and {@link AudioTrack#getTimestamp(AudioTimestamp)}.\n *\n * <p>Call {@link #setAudioTrack(AudioTrack, int, int, int)} to set the audio track to wrap. Call\n * {@link #mayHandleBuffer(long)} if there is input data to write to the track. If it returns false,\n * the audio track position is stabilizing and no data may be written. Call {@link #start()}\n * immediately before calling {@link AudioTrack#play()}. Call {@link #pause()} when pausing the\n * track. Call {@link #handleEndOfStream(long)} when no more data will be written to the track. When\n * the audio track will no longer be used, call {@link #reset()}.\n */\n/* package */ final class AudioTrackPositionTracker {\n\n  /** Listener for position tracker events. */\n  public interface Listener {\n\n    /**\n     * Called when the frame position is too far from the expected frame position.\n     *\n     * @param audioTimestampPositionFrames The frame position of the last known audio track\n     *     timestamp.\n     * @param audioTimestampSystemTimeUs The system time associated with the last known audio track\n     *     timestamp, in microseconds.\n     * @param systemTimeUs The current time.\n     * @param playbackPositionUs The current playback head position in microseconds.\n     */\n    void onPositionFramesMismatch(\n            long audioTimestampPositionFrames,\n            long audioTimestampSystemTimeUs,\n            long systemTimeUs,\n            long playbackPositionUs);\n\n    /**\n     * Called when the system time associated with the last known audio track timestamp is\n     * unexpectedly far from the current time.\n     *\n     * @param audioTimestampPositionFrames The frame position of the last known audio track\n     *     timestamp.\n     * @param audioTimestampSystemTimeUs The system time associated with the last known audio track\n     *     timestamp, in microseconds.\n     * @param systemTimeUs The current time.\n     * @param playbackPositionUs The current playback head position in microseconds.\n     */\n    void onSystemTimeUsMismatch(\n            long audioTimestampPositionFrames,\n            long audioTimestampSystemTimeUs,\n            long systemTimeUs,\n            long playbackPositionUs);\n\n    /**\n     * Called when the audio track has provided an invalid latency.\n     *\n     * @param latencyUs The reported latency in microseconds.\n     */\n    void onInvalidLatency(long latencyUs);\n\n    /**\n     * Called when the audio track runs out of data to play.\n     *\n     * @param bufferSize The size of the sink's buffer, in bytes.\n     * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for\n     *     PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the\n     *     buffered media can have a variable bitrate so the duration may be unknown.\n     */\n    void onUnderrun(int bufferSize, long bufferSizeMs);\n  }\n\n  /** {@link AudioTrack} playback states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING})\n  private @interface PlayState {}\n  /** @see AudioTrack#PLAYSTATE_STOPPED */\n  private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED;\n  /** @see AudioTrack#PLAYSTATE_PAUSED */\n  private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED;\n  /** @see AudioTrack#PLAYSTATE_PLAYING */\n  private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING;\n\n  /**\n   * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more than\n   * this amount.\n   *\n   * <p>This is a fail safe that should not be required on correctly functioning devices.\n   */\n  private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;\n\n  /**\n   * AudioTrack latencies are deemed impossibly large if they are greater than this amount.\n   *\n   * <p>This is a fail safe that should not be required on correctly functioning devices.\n   */\n  private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;\n\n  private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200;\n\n  private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;\n  private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;\n  private static final int MIN_LATENCY_SAMPLE_INTERVAL_US = 500000;\n\n  private final Listener listener;\n  private final long[] playheadOffsets;\n\n  @Nullable private AudioTrack audioTrack;\n  private int outputPcmFrameSize;\n  private int bufferSize;\n  @Nullable private AudioTimestampPoller audioTimestampPoller;\n  private int outputSampleRate;\n  private boolean needsPassthroughWorkarounds;\n  private long bufferSizeUs;\n\n  private long smoothedPlayheadOffsetUs;\n  private long lastPlayheadSampleTimeUs;\n\n  @Nullable private Method getLatencyMethod;\n  private long latencyUs;\n  private boolean hasData;\n\n  private boolean isOutputPcm;\n  private long lastLatencySampleTimeUs;\n  private long lastRawPlaybackHeadPosition;\n  private long rawPlaybackHeadWrapCount;\n  private long passthroughWorkaroundPauseOffset;\n  private int nextPlayheadOffsetIndex;\n  private int playheadOffsetCount;\n  private long stopTimestampUs;\n  private long forceResetWorkaroundTimeMs;\n  private long stopPlaybackHeadPosition;\n  private long endPlaybackHeadPosition;\n\n  /**\n   * Creates a new audio track position tracker.\n   *\n   * @param listener A listener for position tracking events.\n   */\n  public AudioTrackPositionTracker(Listener listener) {\n    this.listener = Assertions.checkNotNull(listener);\n    if (Util.SDK_INT >= 18) {\n      try {\n        getLatencyMethod = AudioTrack.class.getMethod(\"getLatency\", (Class<?>[]) null);\n      } catch (NoSuchMethodException e) {\n        // There's no guarantee this method exists. Do nothing.\n      }\n    }\n    playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];\n  }\n\n  /**\n   * Sets the {@link AudioTrack} to wrap. Subsequent method calls on this instance relate to this\n   * track's position, until the next call to {@link #reset()}.\n   *\n   * @param audioTrack The audio track to wrap.\n   * @param outputEncoding The encoding of the audio track.\n   * @param outputPcmFrameSize For PCM output encodings, the frame size. The value is ignored\n   *     otherwise.\n   * @param bufferSize The audio track buffer size in bytes.\n   */\n  public void setAudioTrack(\n      AudioTrack audioTrack,\n      @C.Encoding int outputEncoding,\n      int outputPcmFrameSize,\n      int bufferSize) {\n    this.audioTrack = audioTrack;\n    this.outputPcmFrameSize = outputPcmFrameSize;\n    this.bufferSize = bufferSize;\n    audioTimestampPoller = new AudioTimestampPoller(audioTrack);\n    outputSampleRate = audioTrack.getSampleRate();\n    needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding);\n    isOutputPcm = Util.isEncodingLinearPcm(outputEncoding);\n    bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;\n    lastRawPlaybackHeadPosition = 0;\n    rawPlaybackHeadWrapCount = 0;\n    passthroughWorkaroundPauseOffset = 0;\n    hasData = false;\n    stopTimestampUs = C.TIME_UNSET;\n    forceResetWorkaroundTimeMs = C.TIME_UNSET;\n    latencyUs = 0;\n  }\n\n  public long getCurrentPositionUs(boolean sourceEnded) {\n    if (Assertions.checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING) {\n      maybeSampleSyncParams();\n    }\n\n    // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.\n    // Otherwise, derive a smoothed position by sampling the track's frame position.\n    long systemTimeUs = System.nanoTime() / 1000;\n    AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller);\n    if (audioTimestampPoller.hasTimestamp()) {\n      // Calculate the speed-adjusted position using the timestamp (which may be in the future).\n      long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();\n      long timestampPositionUs = framesToDurationUs(timestampPositionFrames);\n      if (!audioTimestampPoller.isTimestampAdvancing()) {\n        return timestampPositionUs;\n      }\n      long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();\n      return timestampPositionUs + elapsedSinceTimestampUs;\n    } else {\n      long positionUs;\n      if (playheadOffsetCount == 0) {\n        // The AudioTrack has started, but we don't have any samples to compute a smoothed position.\n        positionUs = getPlaybackHeadPositionUs();\n      } else {\n        // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off\n        // the system clock (and a smoothed offset between it and the playhead position) so as to\n        // prevent jitter in the reported positions.\n        positionUs = systemTimeUs + smoothedPlayheadOffsetUs;\n      }\n      if (!sourceEnded) {\n        positionUs -= latencyUs;\n      }\n      return positionUs;\n    }\n  }\n\n  /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */\n  public void start() {\n    Assertions.checkNotNull(audioTimestampPoller).reset();\n  }\n\n  /** Returns whether the audio track is in the playing state. */\n  public boolean isPlaying() {\n    return Assertions.checkNotNull(audioTrack).getPlayState() == PLAYSTATE_PLAYING;\n  }\n\n  /**\n   * Checks the state of the audio track and returns whether the caller can write data to the track.\n   * Notifies {@link Listener#onUnderrun(int, long)} if the track has underrun.\n   *\n   * @param writtenFrames The number of frames that have been written.\n   * @return Whether the caller can write data to the track.\n   */\n  public boolean mayHandleBuffer(long writtenFrames) {\n    @PlayState int playState = Assertions.checkNotNull(audioTrack).getPlayState();\n    if (needsPassthroughWorkarounds) {\n      // An AC-3 audio track continues to play data written while it is paused. Stop writing so its\n      // buffer empties. See [Internal: b/18899620].\n      if (playState == PLAYSTATE_PAUSED) {\n        // We force an underrun to pause the track, so don't notify the listener in this case.\n        hasData = false;\n        return false;\n      }\n\n      // A new AC-3 audio track's playback position continues to increase from the old track's\n      // position for a short time after is has been released. Avoid writing data until the playback\n      // head position actually returns to zero.\n      if (playState == PLAYSTATE_STOPPED && getPlaybackHeadPosition() == 0) {\n        return false;\n      }\n    }\n\n    boolean hadData = hasData;\n    hasData = hasPendingData(writtenFrames);\n    if (hadData && !hasData && playState != PLAYSTATE_STOPPED && listener != null) {\n      listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs));\n    }\n\n    return true;\n  }\n\n  /**\n   * Returns an estimate of the number of additional bytes that can be written to the audio track's\n   * buffer without running out of space.\n   *\n   * <p>May only be called if the output encoding is one of the PCM encodings.\n   *\n   * @param writtenBytes The number of bytes written to the audio track so far.\n   * @return An estimate of the number of bytes that can be written.\n   */\n  public int getAvailableBufferSize(long writtenBytes) {\n    int bytesPending = (int) (writtenBytes - (getPlaybackHeadPosition() * outputPcmFrameSize));\n    return bufferSize - bytesPending;\n  }\n\n  /** Returns whether the track is in an invalid state and must be recreated. */\n  public boolean isStalled(long writtenFrames) {\n    return forceResetWorkaroundTimeMs != C.TIME_UNSET\n        && writtenFrames > 0\n        && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs\n            >= FORCE_RESET_WORKAROUND_TIMEOUT_MS;\n  }\n\n  /**\n   * Records the writing position at which the stream ended, so that the reported position can\n   * continue to increment while remaining data is played out.\n   *\n   * @param writtenFrames The number of frames that have been written.\n   */\n  public void handleEndOfStream(long writtenFrames) {\n    stopPlaybackHeadPosition = getPlaybackHeadPosition();\n    stopTimestampUs = SystemClock.elapsedRealtime() * 1000;\n    endPlaybackHeadPosition = writtenFrames;\n  }\n\n  /**\n   * Returns whether the audio track has any pending data to play out at its current position.\n   *\n   * @param writtenFrames The number of frames written to the audio track.\n   * @return Whether the audio track has any pending data to play out.\n   */\n  public boolean hasPendingData(long writtenFrames) {\n    return writtenFrames > getPlaybackHeadPosition()\n        || forceHasPendingData();\n  }\n\n  /**\n   * Pauses the audio track position tracker, returning whether the audio track needs to be paused\n   * to cause playback to pause. If {@code false} is returned the audio track will pause without\n   * further interaction, as the end of stream has been handled.\n   */\n  public boolean pause() {\n    resetSyncParams();\n    if (stopTimestampUs == C.TIME_UNSET) {\n      // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't\n      // supply an advancing position.\n      Assertions.checkNotNull(audioTimestampPoller).reset();\n      return true;\n    }\n    // We've handled the end of the stream already, so there's no need to pause the track.\n    return false;\n  }\n\n  /**\n   * Resets the position tracker. Should be called when the audio track previous passed to {@link\n   * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use.\n   */\n  public void reset() {\n    resetSyncParams();\n    audioTrack = null;\n    audioTimestampPoller = null;\n  }\n\n  private void maybeSampleSyncParams() {\n    long playbackPositionUs = getPlaybackHeadPositionUs();\n    if (playbackPositionUs == 0) {\n      // The AudioTrack hasn't output anything yet.\n      return;\n    }\n    long systemTimeUs = System.nanoTime() / 1000;\n    if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {\n      // Take a new sample and update the smoothed offset between the system clock and the playhead.\n      playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs;\n      nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;\n      if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {\n        playheadOffsetCount++;\n      }\n      lastPlayheadSampleTimeUs = systemTimeUs;\n      smoothedPlayheadOffsetUs = 0;\n      for (int i = 0; i < playheadOffsetCount; i++) {\n        smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;\n      }\n    }\n\n    if (needsPassthroughWorkarounds) {\n      // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on\n      // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353].\n      return;\n    }\n\n    maybePollAndCheckTimestamp(systemTimeUs, playbackPositionUs);\n    maybeUpdateLatency(systemTimeUs);\n  }\n\n  private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) {\n    AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller);\n    if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) {\n      return;\n    }\n\n    // Perform sanity checks on the timestamp and accept/reject it.\n    long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs();\n    long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();\n    if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {\n      listener.onSystemTimeUsMismatch(\n          audioTimestampPositionFrames,\n          audioTimestampSystemTimeUs,\n          systemTimeUs,\n          playbackPositionUs);\n      audioTimestampPoller.rejectTimestamp();\n    } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs)\n        > MAX_AUDIO_TIMESTAMP_OFFSET_US) {\n      listener.onPositionFramesMismatch(\n          audioTimestampPositionFrames,\n          audioTimestampSystemTimeUs,\n          systemTimeUs,\n          playbackPositionUs);\n      audioTimestampPoller.rejectTimestamp();\n    } else {\n      audioTimestampPoller.acceptTimestamp();\n    }\n  }\n\n  private void maybeUpdateLatency(long systemTimeUs) {\n    if (isOutputPcm\n        && getLatencyMethod != null\n        && systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) {\n      try {\n        // Compute the audio track latency, excluding the latency due to the buffer (leaving\n        // latency due to the mixer and audio hardware driver).\n        latencyUs =\n            castNonNull((Integer) getLatencyMethod.invoke(Assertions.checkNotNull(audioTrack)))\n                    * 1000L\n                - bufferSizeUs;\n        // Sanity check that the latency is non-negative.\n        latencyUs = Math.max(latencyUs, 0);\n        // Sanity check that the latency isn't too large.\n        if (latencyUs > MAX_LATENCY_US) {\n          listener.onInvalidLatency(latencyUs);\n          latencyUs = 0;\n        }\n      } catch (Exception e) {\n        // The method existed, but doesn't work. Don't try again.\n        getLatencyMethod = null;\n      }\n      lastLatencySampleTimeUs = systemTimeUs;\n    }\n  }\n\n  private long framesToDurationUs(long frameCount) {\n    return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;\n  }\n\n  private void resetSyncParams() {\n    smoothedPlayheadOffsetUs = 0;\n    playheadOffsetCount = 0;\n    nextPlayheadOffsetIndex = 0;\n    lastPlayheadSampleTimeUs = 0;\n  }\n\n  /**\n   * If passthrough workarounds are enabled, pausing is implemented by forcing the AudioTrack to\n   * underrun. In this case, still behave as if we have pending data, otherwise writing won't\n   * resume.\n   */\n  private boolean forceHasPendingData() {\n    return needsPassthroughWorkarounds\n        && Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED\n        && getPlaybackHeadPosition() == 0;\n  }\n\n  /**\n   * Returns whether to work around problems with passthrough audio tracks. See [Internal:\n   * b/18899620, b/19187573, b/21145353].\n   */\n  private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncoding) {\n    return Util.SDK_INT < 23\n        && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);\n  }\n\n  private long getPlaybackHeadPositionUs() {\n    return framesToDurationUs(getPlaybackHeadPosition());\n  }\n\n  /**\n   * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an\n   * unsigned 32 bit integer, which also wraps around periodically. This method returns the playback\n   * head position as a long that will only wrap around if the value exceeds {@link Long#MAX_VALUE}\n   * (which in practice will never happen).\n   *\n   * @return The playback head position, in frames.\n   */\n  private long getPlaybackHeadPosition() {\n    AudioTrack audioTrack = Assertions.checkNotNull(this.audioTrack);\n    if (stopTimestampUs != C.TIME_UNSET) {\n      // Simulate the playback head position up to the total number of frames submitted.\n      long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;\n      long framesSinceStop = (elapsedTimeSinceStopUs * outputSampleRate) / C.MICROS_PER_SECOND;\n      return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);\n    }\n\n    int state = audioTrack.getPlayState();\n    if (state == PLAYSTATE_STOPPED) {\n      // The audio track hasn't been started.\n      return 0;\n    }\n\n    long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();\n    if (needsPassthroughWorkarounds) {\n      // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22\n      // where the playback head position jumps back to zero on paused passthrough/direct audio\n      // tracks. See [Internal: b/19187573].\n      if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {\n        passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;\n      }\n      rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;\n    }\n\n    if (Util.SDK_INT <= 29) {\n      if (rawPlaybackHeadPosition == 0\n          && lastRawPlaybackHeadPosition > 0\n          && state == PLAYSTATE_PLAYING) {\n        // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state\n        // where its Java API is in the playing state, but the native track is stopped. When this\n        // happens the playback head position gets stuck at zero. In this case, return the old\n        // playback head position and force the track to be reset after\n        // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed.\n        if (forceResetWorkaroundTimeMs == C.TIME_UNSET) {\n          forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime();\n        }\n        return lastRawPlaybackHeadPosition;\n      } else {\n        forceResetWorkaroundTimeMs = C.TIME_UNSET;\n      }\n    }\n\n    if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {\n      // The value must have wrapped around.\n      rawPlaybackHeadWrapCount++;\n    }\n    lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;\n    return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.media.AudioTrack;\nimport android.media.audiofx.AudioEffect;\nimport androidx.annotation.Nullable;\n\n/**\n * Represents auxiliary effect information, which can be used to attach an auxiliary effect to an\n * underlying {@link AudioTrack}.\n *\n * <p>Auxiliary effects can only be applied if the application has the {@code\n * android.permission.MODIFY_AUDIO_SETTINGS} permission. Apps are responsible for retaining the\n * associated audio effect instance and releasing it when it's no longer needed. See the\n * documentation of {@link AudioEffect} for more information.\n */\npublic final class AuxEffectInfo {\n\n  /** Value for {@link #effectId} representing no auxiliary effect. */\n  public static final int NO_AUX_EFFECT_ID = 0;\n\n  /**\n   * The identifier of the effect, or {@link #NO_AUX_EFFECT_ID} if there is no effect.\n   *\n   * @see AudioTrack#attachAuxEffect(int)\n   */\n  public final int effectId;\n  /**\n   * The send level for the effect.\n   *\n   * @see AudioTrack#setAuxEffectSendLevel(float)\n   */\n  public final float sendLevel;\n\n  /**\n   * Creates an instance with the given effect identifier and send level.\n   *\n   * @param effectId The effect identifier. This is the value returned by {@link\n   *     AudioEffect#getId()} on the effect, or {@value NO_AUX_EFFECT_ID} which represents no\n   *     effect. This value is passed to {@link AudioTrack#attachAuxEffect(int)} on the underlying\n   *     audio track.\n   * @param sendLevel The send level for the effect, where 0 represents no effect and a value of 1\n   *     is full send. If {@code effectId} is not {@value #NO_AUX_EFFECT_ID}, this value is passed\n   *     to {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track.\n   */\n  public AuxEffectInfo(int effectId, float sendLevel) {\n    this.effectId = effectId;\n    this.sendLevel = sendLevel;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    AuxEffectInfo auxEffectInfo = (AuxEffectInfo) o;\n    return effectId == auxEffectInfo.effectId\n        && Float.compare(auxEffectInfo.sendLevel, sendLevel) == 0;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + effectId;\n    result = 31 * result + Float.floatToIntBits(sendLevel);\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.CallSuper;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * Base class for audio processors that keep an output buffer and an internal buffer that is reused\n * whenever input is queued. Subclasses should override {@link #onConfigure(AudioFormat)} to return\n * the output audio format for the processor if it's active.\n */\npublic abstract class BaseAudioProcessor implements AudioProcessor {\n\n  /** The current input audio format. */\n  protected AudioFormat inputAudioFormat;\n  /** The current output audio format. */\n  protected AudioFormat outputAudioFormat;\n\n  private AudioFormat pendingInputAudioFormat;\n  private AudioFormat pendingOutputAudioFormat;\n  private ByteBuffer buffer;\n  private ByteBuffer outputBuffer;\n  private boolean inputEnded;\n\n  public BaseAudioProcessor() {\n    buffer = EMPTY_BUFFER;\n    outputBuffer = EMPTY_BUFFER;\n    pendingInputAudioFormat = AudioFormat.NOT_SET;\n    pendingOutputAudioFormat = AudioFormat.NOT_SET;\n    inputAudioFormat = AudioFormat.NOT_SET;\n    outputAudioFormat = AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public final AudioFormat configure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    pendingInputAudioFormat = inputAudioFormat;\n    pendingOutputAudioFormat = onConfigure(inputAudioFormat);\n    return isActive() ? pendingOutputAudioFormat : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public boolean isActive() {\n    return pendingOutputAudioFormat != AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public final void queueEndOfStream() {\n    inputEnded = true;\n    onQueueEndOfStream();\n  }\n\n  @CallSuper\n  @Override\n  public ByteBuffer getOutput() {\n    ByteBuffer outputBuffer = this.outputBuffer;\n    this.outputBuffer = EMPTY_BUFFER;\n    return outputBuffer;\n  }\n\n  @CallSuper\n  @SuppressWarnings(\"ReferenceEquality\")\n  @Override\n  public boolean isEnded() {\n    return inputEnded && outputBuffer == EMPTY_BUFFER;\n  }\n\n  @Override\n  public final void flush() {\n    outputBuffer = EMPTY_BUFFER;\n    inputEnded = false;\n    inputAudioFormat = pendingInputAudioFormat;\n    outputAudioFormat = pendingOutputAudioFormat;\n    onFlush();\n  }\n\n  @Override\n  public final void reset() {\n    flush();\n    buffer = EMPTY_BUFFER;\n    pendingInputAudioFormat = AudioFormat.NOT_SET;\n    pendingOutputAudioFormat = AudioFormat.NOT_SET;\n    inputAudioFormat = AudioFormat.NOT_SET;\n    outputAudioFormat = AudioFormat.NOT_SET;\n    onReset();\n  }\n\n  /**\n   * Replaces the current output buffer with a buffer of at least {@code count} bytes and returns\n   * it. Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be\n   * read via {@link #getOutput()}.\n   */\n  protected final ByteBuffer replaceOutputBuffer(int count) {\n    if (buffer.capacity() < count) {\n      buffer = ByteBuffer.allocateDirect(count).order(ByteOrder.nativeOrder());\n    } else {\n      buffer.clear();\n    }\n    outputBuffer = buffer;\n    return buffer;\n  }\n\n  /** Returns whether the current output buffer has any data remaining. */\n  protected final boolean hasPendingOutput() {\n    return outputBuffer.hasRemaining();\n  }\n\n  /** Called when the processor is configured for a new input format. */\n  protected AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    return AudioFormat.NOT_SET;\n  }\n\n  /** Called when the end-of-stream is queued to the processor. */\n  protected void onQueueEndOfStream() {\n    // Do nothing.\n  }\n\n  /** Called when the processor is flushed, directly or as part of resetting. */\n  protected void onFlush() {\n    // Do nothing.\n  }\n\n  /** Called when the processor is reset. */\n  protected void onReset() {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.nio.ByteBuffer;\n\n/**\n * An {@link AudioProcessor} that applies a mapping from input channels onto specified output\n * channels. This can be used to reorder, duplicate or discard channels.\n */\n@SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor {\n\n  @Nullable private int[] pendingOutputChannels;\n  @Nullable private int[] outputChannels;\n\n  /**\n   * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to\n   * start using the new channel map.\n   *\n   * @param outputChannels The mapping from input to output channel indices, or {@code null} to\n   *     leave the input unchanged.\n   * @see AudioSink#configure(int, int, int, int, int[], int, int)\n   */\n  public void setChannelMap(@Nullable int[] outputChannels) {\n    pendingOutputChannels = outputChannels;\n  }\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    @Nullable int[] outputChannels = pendingOutputChannels;\n    if (outputChannels == null) {\n      return AudioFormat.NOT_SET;\n    }\n\n    if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n\n    boolean active = inputAudioFormat.channelCount != outputChannels.length;\n    for (int i = 0; i < outputChannels.length; i++) {\n      int channelIndex = outputChannels[i];\n      if (channelIndex >= inputAudioFormat.channelCount) {\n        throw new UnhandledAudioFormatException(inputAudioFormat);\n      }\n      active |= (channelIndex != i);\n    }\n    return active\n        ? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT)\n        : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    int[] outputChannels = Assertions.checkNotNull(this.outputChannels);\n    int position = inputBuffer.position();\n    int limit = inputBuffer.limit();\n    int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame;\n    int outputSize = frameCount * outputAudioFormat.bytesPerFrame;\n    ByteBuffer buffer = replaceOutputBuffer(outputSize);\n    while (position < limit) {\n      for (int channelIndex : outputChannels) {\n        buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));\n      }\n      position += inputAudioFormat.bytesPerFrame;\n    }\n    inputBuffer.position(limit);\n    buffer.flip();\n  }\n\n  @Override\n  protected void onFlush() {\n    outputChannels = pendingOutputChannels;\n  }\n\n  @Override\n  protected void onReset() {\n    outputChannels = null;\n    pendingOutputChannels = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.media.AudioFormat;\nimport android.media.AudioManager;\nimport android.media.AudioTrack;\nimport android.os.ConditionVariable;\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Collections;\n\n/**\n * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback\n * position smoothing, non-blocking writes and reconfiguration.\n * <p>\n * If tunneling mode is enabled, care must be taken that audio processors do not output buffers with\n * a different duration than their input, and buffer processors must produce output corresponding to\n * their last input immediately after that input is queued. This means that, for example, speed\n * adjustment is not possible while using tunneling.\n */\npublic final class DefaultAudioSink implements AudioSink {\n\n  /**\n   * Thrown when the audio track has provided a spurious timestamp, if {@link\n   * #failOnSpuriousAudioTimestamp} is set.\n   */\n  public static final class InvalidAudioTrackTimestampException extends RuntimeException {\n\n    /**\n     * Creates a new invalid timestamp exception with the specified message.\n     *\n     * @param message The detail message for this exception.\n     */\n    private InvalidAudioTrackTimestampException(String message) {\n      super(message);\n    }\n\n  }\n\n  /**\n   * Provides a chain of audio processors, which are used for any user-defined processing and\n   * applying playback parameters (if supported). Because applying playback parameters can skip and\n   * stretch/compress audio, the sink will query the chain for information on how to transform its\n   * output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link\n   * #getSkippedOutputFrameCount()}.\n   */\n  public interface AudioProcessorChain {\n\n    /**\n     * Returns the fixed chain of audio processors that will process audio. This method is called\n     * once during initialization, but audio processors may change state to become active/inactive\n     * during playback.\n     */\n    AudioProcessor[] getAudioProcessors();\n\n    /**\n     * Configures audio processors to apply the specified playback parameters immediately, returning\n     * the new parameters, which may differ from those passed in. Only called when processors have\n     * no input pending.\n     *\n     * @param playbackParameters The playback parameters to try to apply.\n     * @return The playback parameters that were actually applied.\n     */\n    PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters);\n\n    /**\n     * Scales the specified playout duration to take into account speedup due to audio processing,\n     * returning an input media duration, in arbitrary units.\n     */\n    long getMediaDuration(long playoutDuration);\n\n    /**\n     * Returns the number of output audio frames skipped since the audio processors were last\n     * flushed.\n     */\n    long getSkippedOutputFrameCount();\n  }\n\n  /**\n   * The default audio processor chain, which applies a (possibly empty) chain of user-defined audio\n   * processors followed by {@link SilenceSkippingAudioProcessor} and {@link SonicAudioProcessor}.\n   */\n  public static class DefaultAudioProcessorChain implements AudioProcessorChain {\n\n    private final AudioProcessor[] audioProcessors;\n    private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor;\n    private final SonicAudioProcessor sonicAudioProcessor;\n\n    /**\n     * Creates a new default chain of audio processors, with the user-defined {@code\n     * audioProcessors} applied before silence skipping and playback parameters.\n     */\n    public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) {\n      // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array\n      // rather than using Arrays.copyOf.\n      this.audioProcessors = new AudioProcessor[audioProcessors.length + 2];\n      System.arraycopy(\n          /* src= */ audioProcessors,\n          /* srcPos= */ 0,\n          /* dest= */ this.audioProcessors,\n          /* destPos= */ 0,\n          /* length= */ audioProcessors.length);\n      silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();\n      sonicAudioProcessor = new SonicAudioProcessor();\n      this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor;\n      this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor;\n    }\n\n    @Override\n    public AudioProcessor[] getAudioProcessors() {\n      return audioProcessors;\n    }\n\n    @Override\n    public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {\n      silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence);\n      return new PlaybackParameters(\n          sonicAudioProcessor.setSpeed(playbackParameters.speed),\n          sonicAudioProcessor.setPitch(playbackParameters.pitch),\n          playbackParameters.skipSilence);\n    }\n\n    @Override\n    public long getMediaDuration(long playoutDuration) {\n      return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration);\n    }\n\n    @Override\n    public long getSkippedOutputFrameCount() {\n      return silenceSkippingAudioProcessor.getSkippedFrames();\n    }\n  }\n\n  /**\n   * A minimum length for the {@link AudioTrack} buffer, in microseconds.\n   */\n  private static final long MIN_BUFFER_DURATION_US = 250000;\n  /**\n   * A maximum length for the {@link AudioTrack} buffer, in microseconds.\n   */\n  private static final long MAX_BUFFER_DURATION_US = 750000;\n  /**\n   * The length for passthrough {@link AudioTrack} buffers, in microseconds.\n   */\n  private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000;\n  /**\n   * A multiplication factor to apply to the minimum buffer size requested by the underlying\n   * {@link AudioTrack}.\n   */\n  private static final int BUFFER_MULTIPLICATION_FACTOR = 4;\n\n  /** To avoid underruns on some devices (e.g., Broadcom 7271), scale up the AC3 buffer duration. */\n  private static final int AC3_BUFFER_MULTIPLICATION_FACTOR = 2;\n\n  /**\n   * @see AudioTrack#ERROR_BAD_VALUE\n   */\n  private static final int ERROR_BAD_VALUE = AudioTrack.ERROR_BAD_VALUE;\n  /**\n   * @see AudioTrack#MODE_STATIC\n   */\n  private static final int MODE_STATIC = AudioTrack.MODE_STATIC;\n  /**\n   * @see AudioTrack#MODE_STREAM\n   */\n  private static final int MODE_STREAM = AudioTrack.MODE_STREAM;\n  /**\n   * @see AudioTrack#STATE_INITIALIZED\n   */\n  private static final int STATE_INITIALIZED = AudioTrack.STATE_INITIALIZED;\n  /**\n   * @see AudioTrack#WRITE_NON_BLOCKING\n   */\n  @SuppressLint(\"InlinedApi\")\n  private static final int WRITE_NON_BLOCKING = AudioTrack.WRITE_NON_BLOCKING;\n\n  private static final String TAG = \"AudioTrack\";\n\n  /** Represents states of the {@link #startMediaTimeUs} value. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({START_NOT_SET, START_IN_SYNC, START_NEED_SYNC})\n  private @interface StartMediaTimeState {}\n\n  private static final int START_NOT_SET = 0;\n  private static final int START_IN_SYNC = 1;\n  private static final int START_NEED_SYNC = 2;\n\n  /**\n   * Whether to enable a workaround for an issue where an audio effect does not keep its session\n   * active across releasing/initializing a new audio track, on platform builds where\n   * {@link Util#SDK_INT} &lt; 21.\n   * <p>\n   * The flag must be set before creating a player.\n   */\n  public static boolean enablePreV21AudioSessionWorkaround = false;\n\n  /**\n   * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is\n   * reported from {@link AudioTrack#getTimestamp}.\n   * <p>\n   * The flag must be set before creating a player. Should be set to {@code true} for testing and\n   * debugging purposes only.\n   */\n  public static boolean failOnSpuriousAudioTimestamp = false;\n\n  @Nullable private final AudioCapabilities audioCapabilities;\n  private final AudioProcessorChain audioProcessorChain;\n  private final boolean enableConvertHighResIntPcmToFloat;\n  private final ChannelMappingAudioProcessor channelMappingAudioProcessor;\n  private final TrimmingAudioProcessor trimmingAudioProcessor;\n  private final AudioProcessor[] toIntPcmAvailableAudioProcessors;\n  private final AudioProcessor[] toFloatPcmAvailableAudioProcessors;\n  private final ConditionVariable releasingConditionVariable;\n  private final AudioTrackPositionTracker audioTrackPositionTracker;\n  private final ArrayDeque<PlaybackParametersCheckpoint> playbackParametersCheckpoints;\n\n  @Nullable private Listener listener;\n  /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */\n  @Nullable private AudioTrack keepSessionIdAudioTrack;\n\n  @Nullable private Configuration pendingConfiguration;\n  private Configuration configuration;\n  private AudioTrack audioTrack;\n\n  private AudioAttributes audioAttributes;\n  @Nullable private PlaybackParameters afterDrainPlaybackParameters;\n  private PlaybackParameters playbackParameters;\n  private long playbackParametersOffsetUs;\n  private long playbackParametersPositionUs;\n\n  @Nullable private ByteBuffer avSyncHeader;\n  private int bytesUntilNextAvSync;\n\n  private long submittedPcmBytes;\n  private long submittedEncodedFrames;\n  private long writtenPcmBytes;\n  private long writtenEncodedFrames;\n  private int framesPerEncodedSample;\n  private @StartMediaTimeState int startMediaTimeState;\n  private long startMediaTimeUs;\n  private float volume;\n\n  private AudioProcessor[] activeAudioProcessors;\n  private ByteBuffer[] outputBuffers;\n  @Nullable private ByteBuffer inputBuffer;\n  @Nullable private ByteBuffer outputBuffer;\n  private byte[] preV21OutputBuffer;\n  private int preV21OutputBufferOffset;\n  private int drainingAudioProcessorIndex;\n  private boolean handledEndOfStream;\n  private boolean stoppedAudioTrack;\n\n  private boolean playing;\n  private int audioSessionId;\n  private AuxEffectInfo auxEffectInfo;\n  private boolean tunneling;\n  private long lastFeedElapsedRealtimeMs;\n\n  /**\n   * Creates a new default audio sink.\n   *\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before\n   *     output. May be empty.\n   */\n  public DefaultAudioSink(\n      @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) {\n    this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false);\n  }\n\n  /**\n   * Creates a new default audio sink, optionally using float output for high resolution PCM.\n   *\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before\n   *     output. May be empty.\n   * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution\n   *     integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer\n   *     audio processing (for example, speed and pitch adjustment) will not be available when float\n   *     output is in use.\n   */\n  public DefaultAudioSink(\n      @Nullable AudioCapabilities audioCapabilities,\n      AudioProcessor[] audioProcessors,\n      boolean enableConvertHighResIntPcmToFloat) {\n    this(\n        audioCapabilities,\n        new DefaultAudioProcessorChain(audioProcessors),\n        enableConvertHighResIntPcmToFloat);\n  }\n\n  /**\n   * Creates a new default audio sink, optionally using float output for high resolution PCM and\n   * with the specified {@code audioProcessorChain}.\n   *\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback\n   *     parameters adjustments. The instance passed in must not be reused in other sinks.\n   * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution\n   *     integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer\n   *     audio processing (for example, speed and pitch adjustment) will not be available when float\n   *     output is in use.\n   */\n  public DefaultAudioSink(\n      @Nullable AudioCapabilities audioCapabilities,\n      AudioProcessorChain audioProcessorChain,\n      boolean enableConvertHighResIntPcmToFloat) {\n    this.audioCapabilities = audioCapabilities;\n    this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);\n    this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat;\n    releasingConditionVariable = new ConditionVariable(true);\n    audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());\n    channelMappingAudioProcessor = new ChannelMappingAudioProcessor();\n    trimmingAudioProcessor = new TrimmingAudioProcessor();\n    ArrayList<AudioProcessor> toIntPcmAudioProcessors = new ArrayList<>();\n    Collections.addAll(\n        toIntPcmAudioProcessors,\n        new ResamplingAudioProcessor(),\n        channelMappingAudioProcessor,\n        trimmingAudioProcessor);\n    Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors());\n    toIntPcmAvailableAudioProcessors = toIntPcmAudioProcessors.toArray(new AudioProcessor[0]);\n    toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()};\n    volume = 1.0f;\n    startMediaTimeState = START_NOT_SET;\n    audioAttributes = AudioAttributes.DEFAULT;\n    audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n    auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);\n    playbackParameters = PlaybackParameters.DEFAULT;\n    drainingAudioProcessorIndex = C.INDEX_UNSET;\n    activeAudioProcessors = new AudioProcessor[0];\n    outputBuffers = new ByteBuffer[0];\n    playbackParametersCheckpoints = new ArrayDeque<>();\n  }\n\n  // AudioSink implementation.\n\n  @Override\n  public void setListener(Listener listener) {\n    this.listener = listener;\n  }\n\n  @Override\n  public boolean supportsOutput(int channelCount, @C.Encoding int encoding) {\n    if (Util.isEncodingLinearPcm(encoding)) {\n      // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float\n      // output from platform API version 21 only. Other integer PCM encodings are resampled by this\n      // sink to 16-bit PCM. We assume that the audio framework will downsample any number of\n      // channels to the output device's required number of channels.\n      return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21;\n    } else {\n      return audioCapabilities != null\n          && audioCapabilities.supportsEncoding(encoding)\n          && (channelCount == Format.NO_VALUE\n              || channelCount <= audioCapabilities.getMaxChannelCount());\n    }\n  }\n\n  @Override\n  public long getCurrentPositionUs(boolean sourceEnded) {\n    if (!isInitialized() || startMediaTimeState == START_NOT_SET) {\n      return CURRENT_POSITION_NOT_SET;\n    }\n    long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded);\n    positionUs = Math.min(positionUs, configuration.framesToDurationUs(getWrittenFrames()));\n    return startMediaTimeUs + applySkipping(applySpeedup(positionUs));\n  }\n\n  @Override\n  public void configure(\n      @C.Encoding int inputEncoding,\n      int inputChannelCount,\n      int inputSampleRate,\n      int specifiedBufferSize,\n      @Nullable int[] outputChannels,\n      int trimStartFrames,\n      int trimEndFrames)\n      throws ConfigurationException {\n    if (Util.SDK_INT < 21 && inputChannelCount == 8 && outputChannels == null) {\n      // AudioTrack doesn't support 8 channel output before Android L. Discard the last two (side)\n      // channels to give a 6 channel stream that is supported.\n      outputChannels = new int[6];\n      for (int i = 0; i < outputChannels.length; i++) {\n        outputChannels[i] = i;\n      }\n    }\n\n    boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding);\n    boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;\n    int sampleRate = inputSampleRate;\n    int channelCount = inputChannelCount;\n    @C.Encoding int encoding = inputEncoding;\n    boolean shouldConvertHighResIntPcmToFloat =\n        enableConvertHighResIntPcmToFloat\n            && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT)\n            && Util.isEncodingHighResolutionIntegerPcm(inputEncoding);\n    AudioProcessor[] availableAudioProcessors =\n        shouldConvertHighResIntPcmToFloat\n            ? toFloatPcmAvailableAudioProcessors\n            : toIntPcmAvailableAudioProcessors;\n    if (processingEnabled) {\n      trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames);\n      channelMappingAudioProcessor.setChannelMap(outputChannels);\n      AudioProcessor.AudioFormat inputAudioFormat =\n          new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding);\n      AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat;\n      for (AudioProcessor audioProcessor : availableAudioProcessors) {\n        try {\n          outputAudioFormat = audioProcessor.configure(inputAudioFormat);\n        } catch (UnhandledAudioFormatException e) {\n          throw new ConfigurationException(e);\n        }\n        if (audioProcessor.isActive()) {\n          inputAudioFormat = outputAudioFormat;\n        }\n      }\n      sampleRate = outputAudioFormat.sampleRate;\n      channelCount = outputAudioFormat.channelCount;\n      encoding = outputAudioFormat.encoding;\n    }\n\n    int outputChannelConfig = getChannelConfig(channelCount, isInputPcm);\n    if (outputChannelConfig == AudioFormat.CHANNEL_INVALID) {\n      throw new ConfigurationException(\"Unsupported channel count: \" + channelCount);\n    }\n\n    int inputPcmFrameSize =\n        isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET;\n    int outputPcmFrameSize =\n        isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET;\n    boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat;\n    Configuration pendingConfiguration =\n        new Configuration(\n            isInputPcm,\n            inputPcmFrameSize,\n            inputSampleRate,\n            outputPcmFrameSize,\n            sampleRate,\n            outputChannelConfig,\n            encoding,\n            specifiedBufferSize,\n            processingEnabled,\n            canApplyPlaybackParameters,\n            availableAudioProcessors);\n    if (isInitialized()) {\n      this.pendingConfiguration = pendingConfiguration;\n    } else {\n      configuration = pendingConfiguration;\n    }\n  }\n\n  private void setupAudioProcessors() {\n    AudioProcessor[] audioProcessors = configuration.availableAudioProcessors;\n    ArrayList<AudioProcessor> newAudioProcessors = new ArrayList<>();\n    for (AudioProcessor audioProcessor : audioProcessors) {\n      if (audioProcessor.isActive()) {\n        newAudioProcessors.add(audioProcessor);\n      } else {\n        audioProcessor.flush();\n      }\n    }\n    int count = newAudioProcessors.size();\n    activeAudioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);\n    outputBuffers = new ByteBuffer[count];\n    flushAudioProcessors();\n  }\n\n  private void flushAudioProcessors() {\n    for (int i = 0; i < activeAudioProcessors.length; i++) {\n      AudioProcessor audioProcessor = activeAudioProcessors[i];\n      audioProcessor.flush();\n      outputBuffers[i] = audioProcessor.getOutput();\n    }\n  }\n\n  private void initialize(long presentationTimeUs) throws InitializationException {\n    // If we're asynchronously releasing a previous audio track then we block until it has been\n    // released. This guarantees that we cannot end up in a state where we have multiple audio\n    // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust\n    // the shared memory that's available for audio track buffers. This would in turn cause the\n    // initialization of the audio track to fail.\n    releasingConditionVariable.block();\n\n    audioTrack =\n        Assertions.checkNotNull(configuration)\n            .buildAudioTrack(tunneling, audioAttributes, audioSessionId);\n    int audioSessionId = audioTrack.getAudioSessionId();\n    if (enablePreV21AudioSessionWorkaround) {\n      if (Util.SDK_INT < 21) {\n        // The workaround creates an audio track with a two byte buffer on the same session, and\n        // does not release it until this object is released, which keeps the session active.\n        if (keepSessionIdAudioTrack != null\n            && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {\n          releaseKeepSessionIdAudioTrack();\n        }\n        if (keepSessionIdAudioTrack == null) {\n          keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId);\n        }\n      }\n    }\n    if (this.audioSessionId != audioSessionId) {\n      this.audioSessionId = audioSessionId;\n      if (listener != null) {\n        listener.onAudioSessionId(audioSessionId);\n      }\n    }\n\n    applyPlaybackParameters(playbackParameters, presentationTimeUs);\n\n    audioTrackPositionTracker.setAudioTrack(\n        audioTrack,\n        configuration.outputEncoding,\n        configuration.outputPcmFrameSize,\n        configuration.bufferSize);\n    setVolumeInternal();\n\n    if (auxEffectInfo.effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) {\n      audioTrack.attachAuxEffect(auxEffectInfo.effectId);\n      audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel);\n    }\n  }\n\n  @Override\n  public void play() {\n    playing = true;\n    if (isInitialized()) {\n      audioTrackPositionTracker.start();\n      audioTrack.play();\n    }\n  }\n\n  @Override\n  public void handleDiscontinuity() {\n    // Force resynchronization after a skipped buffer.\n    if (startMediaTimeState == START_IN_SYNC) {\n      startMediaTimeState = START_NEED_SYNC;\n    }\n  }\n\n  @Override\n  @SuppressWarnings(\"ReferenceEquality\")\n  public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)\n      throws InitializationException, WriteException {\n    Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer);\n\n    if (pendingConfiguration != null) {\n      if (!drainAudioProcessorsToEndOfStream()) {\n        // There's still pending data in audio processors to write to the track.\n        return false;\n      } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) {\n        playPendingData();\n        if (hasPendingData()) {\n          // We're waiting for playout on the current audio track to finish.\n          return false;\n        }\n        flush();\n      } else {\n        // The current audio track can be reused for the new configuration.\n        configuration = pendingConfiguration;\n        pendingConfiguration = null;\n      }\n      // Re-apply playback parameters.\n      applyPlaybackParameters(playbackParameters, presentationTimeUs);\n    }\n\n    if (!isInitialized()) {\n      initialize(presentationTimeUs);\n      if (playing) {\n        play();\n      }\n    }\n\n    if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) {\n      return false;\n    }\n\n    if (inputBuffer == null) {\n      // We are seeing this buffer for the first time.\n      if (!buffer.hasRemaining()) {\n        // The buffer is empty.\n        return true;\n      }\n\n      if (!configuration.isInputPcm && framesPerEncodedSample == 0) {\n        // If this is the first encoded sample, calculate the sample size in frames.\n        framesPerEncodedSample = getFramesPerEncodedSample(configuration.outputEncoding, buffer);\n        if (framesPerEncodedSample == 0) {\n          // We still don't know the number of frames per sample, so drop the buffer.\n          // For TrueHD this can occur after some seek operations, as not every sample starts with\n          // a syncframe header. If we chunked samples together so the extracted samples always\n          // started with a syncframe header, the chunks would be too large.\n          return true;\n        }\n      }\n\n      if (afterDrainPlaybackParameters != null) {\n        if (!drainAudioProcessorsToEndOfStream()) {\n          // Don't process any more input until draining completes.\n          return false;\n        }\n        PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters;\n        afterDrainPlaybackParameters = null;\n        applyPlaybackParameters(newPlaybackParameters, presentationTimeUs);\n      }\n\n      if (startMediaTimeState == START_NOT_SET) {\n        startMediaTimeUs = Math.max(0, presentationTimeUs);\n        startMediaTimeState = START_IN_SYNC;\n      } else {\n        // Sanity check that presentationTimeUs is consistent with the expected value.\n        long expectedPresentationTimeUs =\n            startMediaTimeUs\n                + configuration.inputFramesToDurationUs(\n                    getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount());\n        if (startMediaTimeState == START_IN_SYNC\n            && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {\n          Log.e(TAG, \"Discontinuity detected [expected \" + expectedPresentationTimeUs + \", got \"\n              + presentationTimeUs + \"]\");\n          startMediaTimeState = START_NEED_SYNC;\n        }\n        if (startMediaTimeState == START_NEED_SYNC) {\n          // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the\n          // number of bytes submitted.\n          long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs;\n          startMediaTimeUs += adjustmentUs;\n          startMediaTimeState = START_IN_SYNC;\n          if (listener != null && adjustmentUs != 0) {\n            listener.onPositionDiscontinuity();\n          }\n        }\n      }\n\n      if (configuration.isInputPcm) {\n        submittedPcmBytes += buffer.remaining();\n      } else {\n        submittedEncodedFrames += framesPerEncodedSample;\n      }\n\n      inputBuffer = buffer;\n    }\n\n    if (configuration.processingEnabled) {\n      processBuffers(presentationTimeUs);\n    } else {\n      writeBuffer(inputBuffer, presentationTimeUs);\n    }\n\n    if (!inputBuffer.hasRemaining()) {\n      inputBuffer = null;\n      return true;\n    }\n\n    if (audioTrackPositionTracker.isStalled(getWrittenFrames())) {\n      Log.w(TAG, \"Resetting stalled audio track\");\n      flush();\n      return true;\n    }\n\n    return false;\n  }\n\n  private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {\n    int count = activeAudioProcessors.length;\n    int index = count;\n    while (index >= 0) {\n      ByteBuffer input = index > 0 ? outputBuffers[index - 1]\n          : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER);\n      if (index == count) {\n        writeBuffer(input, avSyncPresentationTimeUs);\n      } else {\n        AudioProcessor audioProcessor = activeAudioProcessors[index];\n        audioProcessor.queueInput(input);\n        ByteBuffer output = audioProcessor.getOutput();\n        outputBuffers[index] = output;\n        if (output.hasRemaining()) {\n          // Handle the output as input to the next audio processor or the AudioTrack.\n          index++;\n          continue;\n        }\n      }\n\n      if (input.hasRemaining()) {\n        // The input wasn't consumed and no output was produced, so give up for now.\n        return;\n      }\n\n      // Get more input from upstream.\n      index--;\n    }\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException {\n    if (!buffer.hasRemaining()) {\n      return;\n    }\n    if (outputBuffer != null) {\n      Assertions.checkArgument(outputBuffer == buffer);\n    } else {\n      outputBuffer = buffer;\n      if (Util.SDK_INT < 21) {\n        int bytesRemaining = buffer.remaining();\n        if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) {\n          preV21OutputBuffer = new byte[bytesRemaining];\n        }\n        int originalPosition = buffer.position();\n        buffer.get(preV21OutputBuffer, 0, bytesRemaining);\n        buffer.position(originalPosition);\n        preV21OutputBufferOffset = 0;\n      }\n    }\n    int bytesRemaining = buffer.remaining();\n    int bytesWritten = 0;\n    if (Util.SDK_INT < 21) { // isInputPcm == true\n      // Work out how many bytes we can write without the risk of blocking.\n      int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes);\n      if (bytesToWrite > 0) {\n        bytesToWrite = Math.min(bytesRemaining, bytesToWrite);\n        bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);\n        if (bytesWritten > 0) {\n          preV21OutputBufferOffset += bytesWritten;\n          buffer.position(buffer.position() + bytesWritten);\n        }\n      }\n    } else if (tunneling) {\n      Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);\n      bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,\n          avSyncPresentationTimeUs);\n    } else {\n      bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);\n    }\n\n    lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();\n\n    if (bytesWritten < 0) {\n      throw new WriteException(bytesWritten);\n    }\n\n    if (configuration.isInputPcm) {\n      writtenPcmBytes += bytesWritten;\n    }\n    if (bytesWritten == bytesRemaining) {\n      if (!configuration.isInputPcm) {\n        writtenEncodedFrames += framesPerEncodedSample;\n      }\n      outputBuffer = null;\n    }\n  }\n\n  @Override\n  public void playToEndOfStream() throws WriteException {\n    if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) {\n      playPendingData();\n      handledEndOfStream = true;\n    }\n  }\n\n  private boolean drainAudioProcessorsToEndOfStream() throws WriteException {\n    boolean audioProcessorNeedsEndOfStream = false;\n    if (drainingAudioProcessorIndex == C.INDEX_UNSET) {\n      drainingAudioProcessorIndex =\n          configuration.processingEnabled ? 0 : activeAudioProcessors.length;\n      audioProcessorNeedsEndOfStream = true;\n    }\n    while (drainingAudioProcessorIndex < activeAudioProcessors.length) {\n      AudioProcessor audioProcessor = activeAudioProcessors[drainingAudioProcessorIndex];\n      if (audioProcessorNeedsEndOfStream) {\n        audioProcessor.queueEndOfStream();\n      }\n      processBuffers(C.TIME_UNSET);\n      if (!audioProcessor.isEnded()) {\n        return false;\n      }\n      audioProcessorNeedsEndOfStream = true;\n      drainingAudioProcessorIndex++;\n    }\n\n    // Finish writing any remaining output to the track.\n    if (outputBuffer != null) {\n      writeBuffer(outputBuffer, C.TIME_UNSET);\n      if (outputBuffer != null) {\n        return false;\n      }\n    }\n    drainingAudioProcessorIndex = C.INDEX_UNSET;\n    return true;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return !isInitialized() || (handledEndOfStream && !hasPendingData());\n  }\n\n  @Override\n  public boolean hasPendingData() {\n    return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames());\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    if (configuration != null && !configuration.canApplyPlaybackParameters) {\n      this.playbackParameters = PlaybackParameters.DEFAULT;\n      return;\n    }\n    PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters();\n    if (!playbackParameters.equals(lastSetPlaybackParameters)) {\n      if (isInitialized()) {\n        // Drain the audio processors so we can determine the frame position at which the new\n        // parameters apply.\n        afterDrainPlaybackParameters = playbackParameters;\n      } else {\n        // Update the playback parameters now. They will be applied to the audio processors during\n        // initialization.\n        this.playbackParameters = playbackParameters;\n      }\n    }\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    // Mask the already set parameters.\n    return afterDrainPlaybackParameters != null\n        ? afterDrainPlaybackParameters\n        : !playbackParametersCheckpoints.isEmpty()\n            ? playbackParametersCheckpoints.getLast().playbackParameters\n            : playbackParameters;\n  }\n\n  @Override\n  public void setAudioAttributes(AudioAttributes audioAttributes) {\n    if (this.audioAttributes.equals(audioAttributes)) {\n      return;\n    }\n    this.audioAttributes = audioAttributes;\n    if (tunneling) {\n      // The audio attributes are ignored in tunneling mode, so no need to reset.\n      return;\n    }\n    flush();\n    audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n  }\n\n  @Override\n  public void setAudioSessionId(int audioSessionId) {\n    if (this.audioSessionId != audioSessionId) {\n      this.audioSessionId = audioSessionId;\n      flush();\n    }\n  }\n\n  @Override\n  public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {\n    if (this.auxEffectInfo.equals(auxEffectInfo)) {\n      return;\n    }\n    int effectId = auxEffectInfo.effectId;\n    float sendLevel = auxEffectInfo.sendLevel;\n    if (audioTrack != null) {\n      if (this.auxEffectInfo.effectId != effectId) {\n        audioTrack.attachAuxEffect(effectId);\n      }\n      if (effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) {\n        audioTrack.setAuxEffectSendLevel(sendLevel);\n      }\n    }\n    this.auxEffectInfo = auxEffectInfo;\n  }\n\n  @Override\n  public void enableTunnelingV21(int tunnelingAudioSessionId) {\n    Assertions.checkState(Util.SDK_INT >= 21);\n    if (!tunneling || audioSessionId != tunnelingAudioSessionId) {\n      tunneling = true;\n      audioSessionId = tunnelingAudioSessionId;\n      flush();\n    }\n  }\n\n  @Override\n  public void disableTunneling() {\n    if (tunneling) {\n      tunneling = false;\n      audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n      flush();\n    }\n  }\n\n  @Override\n  public void setVolume(float volume) {\n    if (this.volume != volume) {\n      this.volume = volume;\n      setVolumeInternal();\n    }\n  }\n\n  private void setVolumeInternal() {\n    if (!isInitialized()) {\n      // Do nothing.\n    } else if (Util.SDK_INT >= 21) {\n      setVolumeInternalV21(audioTrack, volume);\n    } else {\n      setVolumeInternalV3(audioTrack, volume);\n    }\n  }\n\n  @Override\n  public void pause() {\n    playing = false;\n    if (isInitialized() && audioTrackPositionTracker.pause()) {\n      audioTrack.pause();\n    }\n  }\n\n  @Override\n  public void flush() {\n    if (isInitialized()) {\n      submittedPcmBytes = 0;\n      submittedEncodedFrames = 0;\n      writtenPcmBytes = 0;\n      writtenEncodedFrames = 0;\n      framesPerEncodedSample = 0;\n      if (afterDrainPlaybackParameters != null) {\n        playbackParameters = afterDrainPlaybackParameters;\n        afterDrainPlaybackParameters = null;\n      } else if (!playbackParametersCheckpoints.isEmpty()) {\n        playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters;\n      }\n      playbackParametersCheckpoints.clear();\n      playbackParametersOffsetUs = 0;\n      playbackParametersPositionUs = 0;\n      trimmingAudioProcessor.resetTrimmedFrameCount();\n      flushAudioProcessors();\n      inputBuffer = null;\n      outputBuffer = null;\n      stoppedAudioTrack = false;\n      handledEndOfStream = false;\n      drainingAudioProcessorIndex = C.INDEX_UNSET;\n      avSyncHeader = null;\n      bytesUntilNextAvSync = 0;\n      startMediaTimeState = START_NOT_SET;\n      if (audioTrackPositionTracker.isPlaying()) {\n        audioTrack.pause();\n      }\n      // AudioTrack.release can take some time, so we call it on a background thread.\n      final AudioTrack toRelease = audioTrack;\n      audioTrack = null;\n      if (pendingConfiguration != null) {\n        configuration = pendingConfiguration;\n        pendingConfiguration = null;\n      }\n      audioTrackPositionTracker.reset();\n      releasingConditionVariable.close();\n      new Thread() {\n        @Override\n        public void run() {\n          try {\n            toRelease.flush();\n            toRelease.release();\n          } finally {\n            releasingConditionVariable.open();\n          }\n        }\n      }.start();\n    }\n  }\n\n  @Override\n  public void reset() {\n    flush();\n    releaseKeepSessionIdAudioTrack();\n    for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) {\n      audioProcessor.reset();\n    }\n    for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) {\n      audioProcessor.reset();\n    }\n    audioSessionId = C.AUDIO_SESSION_ID_UNSET;\n    playing = false;\n  }\n\n  /**\n   * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}.\n   */\n  private void releaseKeepSessionIdAudioTrack() {\n    if (keepSessionIdAudioTrack == null) {\n      return;\n    }\n\n    // AudioTrack.release can take some time, so we call it on a background thread.\n    final AudioTrack toRelease = keepSessionIdAudioTrack;\n    keepSessionIdAudioTrack = null;\n    new Thread() {\n      @Override\n      public void run() {\n        toRelease.release();\n      }\n    }.start();\n  }\n\n  private void applyPlaybackParameters(\n      PlaybackParameters playbackParameters, long presentationTimeUs) {\n    PlaybackParameters newPlaybackParameters =\n        configuration.canApplyPlaybackParameters\n            ? audioProcessorChain.applyPlaybackParameters(playbackParameters)\n            : PlaybackParameters.DEFAULT;\n    // Store the position and corresponding media time from which the parameters will apply.\n    playbackParametersCheckpoints.add(\n        new PlaybackParametersCheckpoint(\n            newPlaybackParameters,\n            /* mediaTimeUs= */ Math.max(0, presentationTimeUs),\n            /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames())));\n    setupAudioProcessors();\n  }\n\n  private long applySpeedup(long positionUs) {\n    @Nullable PlaybackParametersCheckpoint checkpoint = null;\n    while (!playbackParametersCheckpoints.isEmpty()\n        && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) {\n      checkpoint = playbackParametersCheckpoints.remove();\n    }\n    if (checkpoint != null) {\n      // We are playing (or about to play) media with the new playback parameters, so update them.\n      playbackParameters = checkpoint.playbackParameters;\n      playbackParametersPositionUs = checkpoint.positionUs;\n      playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs;\n    }\n\n    if (playbackParameters.speed == 1f) {\n      return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs;\n    }\n\n    if (playbackParametersCheckpoints.isEmpty()) {\n      return playbackParametersOffsetUs\n          + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs);\n    }\n\n    // We are playing data at a previous playback speed, so fall back to multiplying by the speed.\n    return playbackParametersOffsetUs\n        + Util.getMediaDurationForPlayoutDuration(\n            positionUs - playbackParametersPositionUs, playbackParameters.speed);\n  }\n\n  private long applySkipping(long positionUs) {\n    return positionUs\n        + configuration.framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount());\n  }\n\n  private boolean isInitialized() {\n    return audioTrack != null;\n  }\n\n  private long getSubmittedFrames() {\n    return configuration.isInputPcm\n        ? (submittedPcmBytes / configuration.inputPcmFrameSize)\n        : submittedEncodedFrames;\n  }\n\n  private long getWrittenFrames() {\n    return configuration.isInputPcm\n        ? (writtenPcmBytes / configuration.outputPcmFrameSize)\n        : writtenEncodedFrames;\n  }\n\n  private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {\n    int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE.\n    int channelConfig = AudioFormat.CHANNEL_OUT_MONO;\n    @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;\n    int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.\n    return new AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, bufferSize,\n        MODE_STATIC, audioSessionId);\n  }\n\n  private static int getChannelConfig(int channelCount, boolean isInputPcm) {\n    if (Util.SDK_INT <= 28 && !isInputPcm) {\n      // In passthrough mode the channel count used to configure the audio track doesn't affect how\n      // the stream is handled, except that some devices do overly-strict channel configuration\n      // checks. Therefore we override the channel count so that a known-working channel\n      // configuration is chosen in all cases. See [Internal: b/29116190].\n      if (channelCount == 7) {\n        channelCount = 8;\n      } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) {\n        channelCount = 6;\n      }\n    }\n\n    // Workaround for Nexus Player not reporting support for mono passthrough.\n    // (See [Internal: b/34268671].)\n    if (Util.SDK_INT <= 26 && \"fugu\".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) {\n      channelCount = 2;\n    }\n\n    return Util.getAudioTrackChannelConfig(channelCount);\n  }\n\n  private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {\n    switch (encoding) {\n      case C.ENCODING_AC3:\n        return 640 * 1000 / 8;\n      case C.ENCODING_E_AC3:\n      case C.ENCODING_E_AC3_JOC:\n        return 6144 * 1000 / 8;\n      case C.ENCODING_AC4:\n        return 2688 * 1000 / 8;\n      case C.ENCODING_DTS:\n        // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.\n        return 1536 * 1000 / 8;\n      case C.ENCODING_DTS_HD:\n        return 18000 * 1000 / 8;\n      case C.ENCODING_DOLBY_TRUEHD:\n        return 24500 * 1000 / 8;\n      case C.ENCODING_INVALID:\n      case C.ENCODING_PCM_16BIT:\n      case C.ENCODING_PCM_24BIT:\n      case C.ENCODING_PCM_32BIT:\n      case C.ENCODING_PCM_8BIT:\n      case C.ENCODING_PCM_A_LAW:\n      case C.ENCODING_PCM_FLOAT:\n      case C.ENCODING_PCM_MU_LAW:\n      case Format.NO_VALUE:\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {\n    if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {\n      return DtsUtil.parseDtsAudioSampleCount(buffer);\n    } else if (encoding == C.ENCODING_AC3) {\n      return Ac3Util.getAc3SyncframeAudioSampleCount();\n    } else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) {\n      return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);\n    } else if (encoding == C.ENCODING_AC4) {\n      return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);\n    } else if (encoding == C.ENCODING_DOLBY_TRUEHD) {\n      int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer);\n      return syncframeOffset == C.INDEX_UNSET\n          ? 0\n          : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)\n              * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);\n    } else {\n      throw new IllegalStateException(\"Unexpected audio encoding: \" + encoding);\n    }\n  }\n\n  @TargetApi(21)\n  private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) {\n    return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);\n  }\n\n  @TargetApi(21)\n  private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size,\n      long presentationTimeUs) {\n    if (Util.SDK_INT >= 26) {\n      // The underlying platform AudioTrack writes AV sync headers directly.\n      return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);\n    }\n    if (avSyncHeader == null) {\n      avSyncHeader = ByteBuffer.allocate(16);\n      avSyncHeader.order(ByteOrder.BIG_ENDIAN);\n      avSyncHeader.putInt(0x55550001);\n    }\n    if (bytesUntilNextAvSync == 0) {\n      avSyncHeader.putInt(4, size);\n      avSyncHeader.putLong(8, presentationTimeUs * 1000);\n      avSyncHeader.position(0);\n      bytesUntilNextAvSync = size;\n    }\n    int avSyncHeaderBytesRemaining = avSyncHeader.remaining();\n    if (avSyncHeaderBytesRemaining > 0) {\n      int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);\n      if (result < 0) {\n        bytesUntilNextAvSync = 0;\n        return result;\n      }\n      if (result < avSyncHeaderBytesRemaining) {\n        return 0;\n      }\n    }\n    int result = writeNonBlockingV21(audioTrack, buffer, size);\n    if (result < 0) {\n      bytesUntilNextAvSync = 0;\n      return result;\n    }\n    bytesUntilNextAvSync -= result;\n    return result;\n  }\n\n  @TargetApi(21)\n  private static void setVolumeInternalV21(AudioTrack audioTrack, float volume) {\n    audioTrack.setVolume(volume);\n  }\n\n  private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) {\n    audioTrack.setStereoVolume(volume, volume);\n  }\n\n  private void playPendingData() {\n    if (!stoppedAudioTrack) {\n      stoppedAudioTrack = true;\n      audioTrackPositionTracker.handleEndOfStream(getWrittenFrames());\n      audioTrack.stop();\n      bytesUntilNextAvSync = 0;\n    }\n  }\n\n  /** Stores playback parameters with the position and media time at which they apply. */\n  private static final class PlaybackParametersCheckpoint {\n\n    private final PlaybackParameters playbackParameters;\n    private final long mediaTimeUs;\n    private final long positionUs;\n\n    private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs,\n        long positionUs) {\n      this.playbackParameters = playbackParameters;\n      this.mediaTimeUs = mediaTimeUs;\n      this.positionUs = positionUs;\n    }\n\n  }\n\n  private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener {\n\n    @Override\n    public void onPositionFramesMismatch(\n        long audioTimestampPositionFrames,\n        long audioTimestampSystemTimeUs,\n        long systemTimeUs,\n        long playbackPositionUs) {\n      String message =\n          \"Spurious audio timestamp (frame position mismatch): \"\n              + audioTimestampPositionFrames\n              + \", \"\n              + audioTimestampSystemTimeUs\n              + \", \"\n              + systemTimeUs\n              + \", \"\n              + playbackPositionUs\n              + \", \"\n              + getSubmittedFrames()\n              + \", \"\n              + getWrittenFrames();\n      if (failOnSpuriousAudioTimestamp) {\n        throw new InvalidAudioTrackTimestampException(message);\n      }\n      Log.w(TAG, message);\n    }\n\n    @Override\n    public void onSystemTimeUsMismatch(\n        long audioTimestampPositionFrames,\n        long audioTimestampSystemTimeUs,\n        long systemTimeUs,\n        long playbackPositionUs) {\n      String message =\n          \"Spurious audio timestamp (system clock mismatch): \"\n              + audioTimestampPositionFrames\n              + \", \"\n              + audioTimestampSystemTimeUs\n              + \", \"\n              + systemTimeUs\n              + \", \"\n              + playbackPositionUs\n              + \", \"\n              + getSubmittedFrames()\n              + \", \"\n              + getWrittenFrames();\n      if (failOnSpuriousAudioTimestamp) {\n        throw new InvalidAudioTrackTimestampException(message);\n      }\n      Log.w(TAG, message);\n    }\n\n    @Override\n    public void onInvalidLatency(long latencyUs) {\n      Log.w(TAG, \"Ignoring impossibly large audio latency: \" + latencyUs);\n    }\n\n    @Override\n    public void onUnderrun(int bufferSize, long bufferSizeMs) {\n      if (listener != null) {\n        long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;\n        listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n      }\n    }\n  }\n\n  /** Stores configuration relating to the audio format. */\n  private static final class Configuration {\n\n    public final boolean isInputPcm;\n    public final int inputPcmFrameSize;\n    public final int inputSampleRate;\n    public final int outputPcmFrameSize;\n    public final int outputSampleRate;\n    public final int outputChannelConfig;\n    @C.Encoding public final int outputEncoding;\n    public final int bufferSize;\n    public final boolean processingEnabled;\n    public final boolean canApplyPlaybackParameters;\n    public final AudioProcessor[] availableAudioProcessors;\n\n    public Configuration(\n        boolean isInputPcm,\n        int inputPcmFrameSize,\n        int inputSampleRate,\n        int outputPcmFrameSize,\n        int outputSampleRate,\n        int outputChannelConfig,\n        int outputEncoding,\n        int specifiedBufferSize,\n        boolean processingEnabled,\n        boolean canApplyPlaybackParameters,\n        AudioProcessor[] availableAudioProcessors) {\n      this.isInputPcm = isInputPcm;\n      this.inputPcmFrameSize = inputPcmFrameSize;\n      this.inputSampleRate = inputSampleRate;\n      this.outputPcmFrameSize = outputPcmFrameSize;\n      this.outputSampleRate = outputSampleRate;\n      this.outputChannelConfig = outputChannelConfig;\n      this.outputEncoding = outputEncoding;\n      this.bufferSize = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize();\n      this.processingEnabled = processingEnabled;\n      this.canApplyPlaybackParameters = canApplyPlaybackParameters;\n      this.availableAudioProcessors = availableAudioProcessors;\n    }\n\n    public boolean canReuseAudioTrack(Configuration audioTrackConfiguration) {\n      return audioTrackConfiguration.outputEncoding == outputEncoding\n          && audioTrackConfiguration.outputSampleRate == outputSampleRate\n          && audioTrackConfiguration.outputChannelConfig == outputChannelConfig;\n    }\n\n    public long inputFramesToDurationUs(long frameCount) {\n      return (frameCount * C.MICROS_PER_SECOND) / inputSampleRate;\n    }\n\n    public long framesToDurationUs(long frameCount) {\n      return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;\n    }\n\n    public long durationUsToFrames(long durationUs) {\n      return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND;\n    }\n\n    public AudioTrack buildAudioTrack(\n        boolean tunneling, AudioAttributes audioAttributes, int audioSessionId)\n        throws InitializationException {\n      AudioTrack audioTrack;\n      if (Util.SDK_INT >= 21) {\n        audioTrack = createAudioTrackV21(tunneling, audioAttributes, audioSessionId);\n      } else {\n        int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);\n        if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {\n          audioTrack =\n              new AudioTrack(\n                  streamType,\n                  outputSampleRate,\n                  outputChannelConfig,\n                  outputEncoding,\n                  bufferSize,\n                  MODE_STREAM);\n        } else {\n          // Re-attach to the same audio session.\n          audioTrack =\n              new AudioTrack(\n                  streamType,\n                  outputSampleRate,\n                  outputChannelConfig,\n                  outputEncoding,\n                  bufferSize,\n                  MODE_STREAM,\n                  audioSessionId);\n        }\n      }\n\n      int state = audioTrack.getState();\n      if (state != STATE_INITIALIZED) {\n        try {\n          audioTrack.release();\n        } catch (Exception e) {\n          // The track has already failed to initialize, so it wouldn't be that surprising if\n          // release were to fail too. Swallow the exception.\n        }\n        throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize);\n      }\n      return audioTrack;\n    }\n\n    @TargetApi(21)\n    private AudioTrack createAudioTrackV21(\n        boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {\n      android.media.AudioAttributes attributes;\n      if (tunneling) {\n        attributes =\n            new android.media.AudioAttributes.Builder()\n                .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)\n                .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC)\n                .setUsage(android.media.AudioAttributes.USAGE_MEDIA)\n                .build();\n      } else {\n        attributes = audioAttributes.getAudioAttributesV21();\n      }\n      AudioFormat format =\n          new AudioFormat.Builder()\n              .setChannelMask(outputChannelConfig)\n              .setEncoding(outputEncoding)\n              .setSampleRate(outputSampleRate)\n              .build();\n      return new AudioTrack(\n          attributes,\n          format,\n          bufferSize,\n          MODE_STREAM,\n          audioSessionId != C.AUDIO_SESSION_ID_UNSET\n              ? audioSessionId\n              : AudioManager.AUDIO_SESSION_ID_GENERATE);\n    }\n\n    private int getDefaultBufferSize() {\n      if (isInputPcm) {\n        int minBufferSize =\n            AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding);\n        Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);\n        int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;\n        int minAppBufferSize =\n            (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;\n        int maxAppBufferSize =\n            (int)\n                Math.max(\n                    minBufferSize, durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);\n        return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);\n      } else {\n        int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding);\n        if (outputEncoding == C.ENCODING_AC3) {\n          rate *= AC3_BUFFER_MULTIPLICATION_FACTOR;\n        }\n        return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\n\n/**\n * Utility methods for parsing DTS frames.\n */\npublic final class DtsUtil {\n\n  private static final int SYNC_VALUE_BE = 0x7FFE8001;\n  private static final int SYNC_VALUE_14B_BE = 0x1FFFE800;\n  private static final int SYNC_VALUE_LE = 0xFE7F0180;\n  private static final int SYNC_VALUE_14B_LE = 0xFF1F00E8;\n  private static final byte FIRST_BYTE_BE = (byte) (SYNC_VALUE_BE >>> 24);\n  private static final byte FIRST_BYTE_14B_BE = (byte) (SYNC_VALUE_14B_BE >>> 24);\n  private static final byte FIRST_BYTE_LE = (byte) (SYNC_VALUE_LE >>> 24);\n  private static final byte FIRST_BYTE_14B_LE = (byte) (SYNC_VALUE_14B_LE >>> 24);\n\n  /**\n   * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4.\n   */\n  private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6,\n      7, 8, 8};\n\n  /**\n   * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5.\n   */\n  private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1,\n      11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1};\n\n  /**\n   * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7.\n   */\n  private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256,\n      384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816,\n      2823, 2944, 3072, 3840, 4096, 6144, 7680};\n\n  /**\n   * Returns whether a given integer matches a DTS sync word. Synchronization and storage modes are\n   * defined in ETSI TS 102 114 V1.1.1 (2002-08), Section 5.3.\n   *\n   * @param word An integer.\n   * @return Whether a given integer matches a DTS sync word.\n   */\n  public static boolean isSyncWord(int word) {\n    return word == SYNC_VALUE_BE\n        || word == SYNC_VALUE_LE\n        || word == SYNC_VALUE_14B_BE\n        || word == SYNC_VALUE_14B_LE;\n  }\n\n  /**\n   * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114\n   * subsections 5.3/5.4.\n   *\n   * @param frame The DTS frame to parse.\n   * @param trackId The track identifier to set on the format.\n   * @param language The language to set on the format.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @return The DTS format parsed from data in the header.\n   */\n  public static Format parseDtsFormat(\n      byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) {\n    ParsableBitArray frameBits = getNormalizedFrameHeader(frame);\n    frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE\n    int amode = frameBits.readBits(6);\n    int channelCount = CHANNELS_BY_AMODE[amode];\n    int sfreq = frameBits.readBits(4);\n    int sampleRate = SAMPLE_RATE_BY_SFREQ[sfreq];\n    int rate = frameBits.readBits(5);\n    int bitrate = rate >= TWICE_BITRATE_KBPS_BY_RATE.length ? Format.NO_VALUE\n        : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2;\n    frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF\n    channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF\n    return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_DTS, null, bitrate,\n        Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);\n  }\n\n  /**\n   * Returns the number of audio samples represented by the given DTS frame.\n   *\n   * @param data The frame to parse.\n   * @return The number of audio samples represented by the frame.\n   */\n  public static int parseDtsAudioSampleCount(byte[] data) {\n    int nblks;\n    switch (data[0]) {\n      case FIRST_BYTE_LE:\n        nblks = ((data[5] & 0x01) << 6) | ((data[4] & 0xFC) >> 2);\n        break;\n      case FIRST_BYTE_14B_LE:\n        nblks = ((data[4] & 0x07) << 4) | ((data[7] & 0x3C) >> 2);\n        break;\n      case FIRST_BYTE_14B_BE:\n        nblks = ((data[5] & 0x07) << 4) | ((data[6] & 0x3C) >> 2);\n        break;\n      default:\n        // We blindly assume FIRST_BYTE_BE if none of the others match.\n        nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2);\n    }\n    return (nblks + 1) * 32;\n  }\n\n  /**\n   * Like {@link #parseDtsAudioSampleCount(byte[])} but reads from a {@link ByteBuffer}. The\n   * buffer's position is not modified.\n   *\n   * @param buffer The {@link ByteBuffer} from which to read.\n   * @return The number of audio samples represented by the syncframe.\n   */\n  public static int parseDtsAudioSampleCount(ByteBuffer buffer) {\n    // See ETSI TS 102 114 subsection 5.4.1.\n    int position = buffer.position();\n    int nblks;\n    switch (buffer.get(position)) {\n      case FIRST_BYTE_LE:\n        nblks = ((buffer.get(position + 5) & 0x01) << 6) | ((buffer.get(position + 4) & 0xFC) >> 2);\n        break;\n      case FIRST_BYTE_14B_LE:\n        nblks = ((buffer.get(position + 4) & 0x07) << 4) | ((buffer.get(position + 7) & 0x3C) >> 2);\n        break;\n      case FIRST_BYTE_14B_BE:\n        nblks = ((buffer.get(position + 5) & 0x07) << 4) | ((buffer.get(position + 6) & 0x3C) >> 2);\n        break;\n      default:\n        // We blindly assume FIRST_BYTE_BE if none of the others match.\n        nblks = ((buffer.get(position + 4) & 0x01) << 6) | ((buffer.get(position + 5) & 0xFC) >> 2);\n    }\n    return (nblks + 1) * 32;\n  }\n\n  /**\n   * Returns the size in bytes of the given DTS frame.\n   *\n   * @param data The frame to parse.\n   * @return The frame's size in bytes.\n   */\n  public static int getDtsFrameSize(byte[] data) {\n    int fsize;\n    boolean uses14BitPerWord = false;\n    switch (data[0]) {\n      case FIRST_BYTE_14B_BE:\n        fsize = (((data[6] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[8] & 0x3C) >> 2)) + 1;\n        uses14BitPerWord = true;\n        break;\n      case FIRST_BYTE_LE:\n        fsize = (((data[4] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[6] & 0xF0) >> 4)) + 1;\n        break;\n      case FIRST_BYTE_14B_LE:\n        fsize = (((data[7] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[9] & 0x3C) >> 2)) + 1;\n        uses14BitPerWord = true;\n        break;\n      default:\n        // We blindly assume FIRST_BYTE_BE if none of the others match.\n        fsize = (((data[5] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[7] & 0xF0) >> 4)) + 1;\n    }\n\n    // If the frame is stored in 14-bit mode, adjust the frame size to reflect the actual byte size.\n    return uses14BitPerWord ? fsize * 16 / 14 : fsize;\n  }\n\n  private static ParsableBitArray getNormalizedFrameHeader(byte[] frameHeader) {\n    if (frameHeader[0] == FIRST_BYTE_BE) {\n      // The frame is already 16-bit mode, big endian.\n      return new ParsableBitArray(frameHeader);\n    }\n    // Data is not normalized, but we don't want to modify frameHeader.\n    frameHeader = Arrays.copyOf(frameHeader, frameHeader.length);\n    if (isLittleEndianFrameHeader(frameHeader)) {\n      // Change endianness.\n      for (int i = 0; i < frameHeader.length - 1; i += 2) {\n        byte temp = frameHeader[i];\n        frameHeader[i] = frameHeader[i + 1];\n        frameHeader[i + 1] = temp;\n      }\n    }\n    ParsableBitArray frameBits = new ParsableBitArray(frameHeader);\n    if (frameHeader[0] == (byte) (SYNC_VALUE_14B_BE >> 24)) {\n      // Discard the 2 most significant bits of each 16 bit word.\n      ParsableBitArray scratchBits = new ParsableBitArray(frameHeader);\n      while (scratchBits.bitsLeft() >= 16) {\n        scratchBits.skipBits(2);\n        frameBits.putInt(scratchBits.readBits(14), 14);\n      }\n    }\n    frameBits.reset(frameHeader);\n    return frameBits;\n  }\n\n  private static boolean isLittleEndianFrameHeader(byte[] frameHeader) {\n    return frameHeader[0] == FIRST_BYTE_LE || frameHeader[0] == FIRST_BYTE_14B_LE;\n  }\n\n  private DtsUtil() {}\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\n\n/**\n * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM\n * audio.\n */\n/* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor {\n\n  private static final int FLOAT_NAN_AS_INT = Float.floatToIntBits(Float.NaN);\n  private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF;\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    if (!Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n    return Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)\n        ? new AudioFormat(\n            inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT)\n        : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    Assertions.checkState(Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding));\n    boolean isInput32Bit = inputAudioFormat.encoding == C.ENCODING_PCM_32BIT;\n    int position = inputBuffer.position();\n    int limit = inputBuffer.limit();\n    int size = limit - position;\n\n    int resampledSize = isInput32Bit ? size : (size / 3) * 4;\n    ByteBuffer buffer = replaceOutputBuffer(resampledSize);\n    if (isInput32Bit) {\n      for (int i = position; i < limit; i += 4) {\n        int pcm32BitInteger =\n            (inputBuffer.get(i) & 0xFF)\n                | ((inputBuffer.get(i + 1) & 0xFF) << 8)\n                | ((inputBuffer.get(i + 2) & 0xFF) << 16)\n                | ((inputBuffer.get(i + 3) & 0xFF) << 24);\n        writePcm32BitFloat(pcm32BitInteger, buffer);\n      }\n    } else { // Input is 24-bit PCM.\n      for (int i = position; i < limit; i += 3) {\n        int pcm32BitInteger =\n            ((inputBuffer.get(i) & 0xFF) << 8)\n                | ((inputBuffer.get(i + 1) & 0xFF) << 16)\n                | ((inputBuffer.get(i + 2) & 0xFF) << 24);\n        writePcm32BitFloat(pcm32BitInteger, buffer);\n      }\n    }\n\n    inputBuffer.position(inputBuffer.limit());\n    buffer.flip();\n  }\n\n  /**\n   * Converts the provided 32-bit integer to a 32-bit float value and writes it to {@code buffer}.\n   *\n   * @param pcm32BitInt The 32-bit integer value to convert to 32-bit float in [-1.0, 1.0].\n   * @param buffer The output buffer.\n   */\n  private static void writePcm32BitFloat(int pcm32BitInt, ByteBuffer buffer) {\n    float pcm32BitFloat = (float) (PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR * pcm32BitInt);\n    int floatBits = Float.floatToIntBits(pcm32BitFloat);\n    if (floatBits == FLOAT_NAN_AS_INT) {\n      floatBits = Float.floatToIntBits((float) 0.0);\n    }\n    buffer.putInt(floatBits);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.media.MediaCrypto;\nimport android.media.MediaFormat;\nimport android.media.audiofx.Virtualizer;\nimport android.os.Handler;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.mediacodec.MediaFormatUtil;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}.\n *\n * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}\n * on the playback thread:\n *\n * <ul>\n *   <li>Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be\n *       a {@link Float} with 0 being silence and 1 being unity gain.\n *   <li>Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The\n *       message payload should be an {@link AudioAttributes}\n *       instance that will configure the underlying audio track.\n *   <li>Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The\n *       message payload should be an {@link AuxEffectInfo} instance that will configure the\n *       underlying audio track.\n * </ul>\n */\npublic class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {\n\n  /**\n   * Maximum number of tracked pending stream change times. Generally there is zero or one pending\n   * stream change. We track more to allow for pending changes that have fewer samples than the\n   * codec latency.\n   */\n  private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10;\n\n  private static final String TAG = \"MediaCodecAudioRenderer\";\n\n  private final Context context;\n  private final EventDispatcher eventDispatcher;\n  private final AudioSink audioSink;\n  private final long[] pendingStreamChangeTimesUs;\n\n  private int codecMaxInputSize;\n  private boolean passthroughEnabled;\n  private boolean codecNeedsDiscardChannelsWorkaround;\n  private boolean codecNeedsEosBufferTimestampWorkaround;\n  private MediaFormat passthroughMediaFormat;\n  @Nullable private Format inputFormat;\n  private long currentPositionUs;\n  private boolean allowFirstBufferPositionDiscontinuity;\n  private boolean allowPositionDiscontinuity;\n  private long lastInputTimeUs;\n  private int pendingStreamChangeCount;\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   */\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) {\n    this(\n        context,\n        mediaCodecSelector,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler,\n   *     AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys) {\n    this(\n        context,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        /* eventHandler= */ null,\n        /* eventListener= */ null);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   */\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener) {\n    this(\n        context,\n        mediaCodecSelector,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler,\n   *     AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener) {\n    this(\n        context,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        eventHandler,\n        eventListener,\n        (AudioCapabilities) null);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before\n   *     output.\n   * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler,\n   *     AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      @Nullable AudioCapabilities audioCapabilities,\n      AudioProcessor... audioProcessors) {\n    this(\n        context,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        eventHandler,\n        eventListener,\n        new DefaultAudioSink(audioCapabilities, audioProcessors));\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioSink The sink to which audio will be output.\n   * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler,\n   *     AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioSink audioSink) {\n    this(\n        context,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        /* enableDecoderFallback= */ false,\n        eventHandler,\n        eventListener,\n        audioSink);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioSink The sink to which audio will be output.\n   */\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      boolean enableDecoderFallback,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioSink audioSink) {\n    this(\n        context,\n        mediaCodecSelector,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        enableDecoderFallback,\n        eventHandler,\n        eventListener,\n        audioSink);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioSink The sink to which audio will be output.\n   * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler,\n   *     AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  public MediaCodecAudioRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean enableDecoderFallback,\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioSink audioSink) {\n    super(\n        C.TRACK_TYPE_AUDIO,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        enableDecoderFallback,\n        /* assumedMinimumCodecOperatingRate= */ 44100);\n    this.context = context.getApplicationContext();\n    this.audioSink = audioSink;\n    lastInputTimeUs = C.TIME_UNSET;\n    pendingStreamChangeTimesUs = new long[MAX_PENDING_STREAM_CHANGE_COUNT];\n    eventDispatcher = new EventDispatcher(eventHandler, eventListener);\n    audioSink.setListener(new AudioSinkListener());\n  }\n\n  @Override\n  @Capabilities\n  protected int supportsFormat(\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      Format format)\n      throws DecoderQueryException {\n    String mimeType = format.sampleMimeType;\n    if (!MimeTypes.isAudio(mimeType)) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n    }\n    @TunnelingSupport\n    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;\n    boolean supportsFormatDrm =\n        format.drmInitData == null\n            || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType)\n            || (format.exoMediaCryptoType == null\n                && supportsFormatDrm(drmSessionManager, format.drmInitData));\n    if (supportsFormatDrm\n        && allowPassthrough(format.channelCount, mimeType)\n        && mediaCodecSelector.getPassthroughDecoderInfo() != null) {\n      return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport);\n    }\n    if ((MimeTypes.AUDIO_RAW.equals(mimeType)\n            && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding))\n        || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {\n      // Assume the decoder outputs 16-bit PCM, unless the input is raw.\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);\n    }\n    List<MediaCodecInfo> decoderInfos =\n        getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false);\n    if (decoderInfos.isEmpty()) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);\n    }\n    if (!supportsFormatDrm) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);\n    }\n    // Check capabilities for the first decoder in the list, which takes priority.\n    MediaCodecInfo decoderInfo = decoderInfos.get(0);\n    boolean isFormatSupported = decoderInfo.isFormatSupported(format);\n    @AdaptiveSupport\n    int adaptiveSupport =\n        isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format)\n            ? ADAPTIVE_SEAMLESS\n            : ADAPTIVE_NOT_SEAMLESS;\n    @FormatSupport\n    int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;\n    return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport);\n  }\n\n  @Override\n  protected List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)\n      throws DecoderQueryException {\n    @Nullable String mimeType = format.sampleMimeType;\n    if (mimeType == null) {\n      return Collections.emptyList();\n    }\n    if (allowPassthrough(format.channelCount, mimeType)) {\n      @Nullable\n      MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();\n      if (passthroughDecoderInfo != null) {\n        return Collections.singletonList(passthroughDecoderInfo);\n      }\n    }\n    List<MediaCodecInfo> decoderInfos =\n        mediaCodecSelector.getDecoderInfos(\n            mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);\n    decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format);\n    if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {\n      // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.\n      List<MediaCodecInfo> decoderInfosWithEac3 = new ArrayList<>(decoderInfos);\n      decoderInfosWithEac3.addAll(\n          mediaCodecSelector.getDecoderInfos(\n              MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false));\n      decoderInfos = decoderInfosWithEac3;\n    }\n    return Collections.unmodifiableList(decoderInfos);\n  }\n\n  /**\n   * Returns whether encoded audio passthrough should be used for playing back the input format.\n   * This implementation returns true if the {@link AudioSink} indicates that encoded audio output\n   * is supported.\n   *\n   * @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if\n   *     not known.\n   * @param mimeType The type of input media.\n   * @return Whether passthrough playback is supported.\n   */\n  protected boolean allowPassthrough(int channelCount, String mimeType) {\n    return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;\n  }\n\n  @Override\n  protected void configureCodec(\n      MediaCodecInfo codecInfo,\n      MediaCodec codec,\n      Format format,\n      @Nullable MediaCrypto crypto,\n      float codecOperatingRate) {\n    codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());\n    codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);\n    codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name);\n    passthroughEnabled = codecInfo.passthrough;\n    String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType;\n    MediaFormat mediaFormat =\n        getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate);\n    codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);\n    if (passthroughEnabled) {\n      // Store the input MIME type if we're using the passthrough codec.\n      passthroughMediaFormat = mediaFormat;\n      passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);\n    } else {\n      passthroughMediaFormat = null;\n    }\n  }\n\n  @Override\n  protected @KeepCodecResult int canKeepCodec(\n      MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {\n    // TODO: We currently rely on recreating the codec when encoder delay or padding is non-zero.\n    // Re-creating the codec is necessary to guarantee that onOutputFormatChanged is called, which\n    // is where encoder delay and padding are propagated to the sink. We should find a better way to\n    // propagate these values, and then allow the codec to be re-used in cases where this would\n    // otherwise be possible.\n    if (getCodecMaxInputSize(codecInfo, newFormat) > codecMaxInputSize\n        || oldFormat.encoderDelay != 0\n        || oldFormat.encoderPadding != 0\n        || newFormat.encoderDelay != 0\n        || newFormat.encoderPadding != 0) {\n      return KEEP_CODEC_RESULT_NO;\n    } else if (codecInfo.isSeamlessAdaptationSupported(\n        oldFormat, newFormat, /* isNewFormatComplete= */ true)) {\n      return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION;\n    } else if (canKeepCodecWithFlush(oldFormat, newFormat)) {\n      return KEEP_CODEC_RESULT_YES_WITH_FLUSH;\n    } else {\n      return KEEP_CODEC_RESULT_NO;\n    }\n  }\n\n  /**\n   * Returns whether the codec can be flushed and reused when switching to a new format. Reuse is\n   * generally possible when the codec would be configured in an identical way after the format\n   * change (excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come\n   * from the {@link Format}).\n   *\n   * @param oldFormat The first format.\n   * @param newFormat The second format.\n   * @return Whether the codec can be flushed and reused when switching to a new format.\n   */\n  protected boolean canKeepCodecWithFlush(Format oldFormat, Format newFormat) {\n    // Flush and reuse the codec if the audio format and initialization data matches. For Opus, we\n    // don't flush and reuse the codec because the decoder may discard samples after flushing, which\n    // would result in audio being dropped just after a stream change (see [Internal: b/143450854]).\n    return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType)\n        && oldFormat.channelCount == newFormat.channelCount\n        && oldFormat.sampleRate == newFormat.sampleRate\n        && oldFormat.pcmEncoding == newFormat.pcmEncoding\n        && oldFormat.initializationDataEquals(newFormat)\n        && !MimeTypes.AUDIO_OPUS.equals(oldFormat.sampleMimeType);\n  }\n\n  @Override\n  @Nullable\n  public MediaClock getMediaClock() {\n    return this;\n  }\n\n  @Override\n  protected float getCodecOperatingRateV23(\n      float operatingRate, Format format, Format[] streamFormats) {\n    // Use the highest known stream sample-rate up front, to avoid having to reconfigure the codec\n    // should an adaptive switch to that stream occur.\n    int maxSampleRate = -1;\n    for (Format streamFormat : streamFormats) {\n      int streamSampleRate = streamFormat.sampleRate;\n      if (streamSampleRate != Format.NO_VALUE) {\n        maxSampleRate = Math.max(maxSampleRate, streamSampleRate);\n      }\n    }\n    return maxSampleRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxSampleRate * operatingRate);\n  }\n\n  @Override\n  protected void onCodecInitialized(String name, long initializedTimestampMs,\n      long initializationDurationMs) {\n    eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);\n  }\n\n  @Override\n  protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {\n    super.onInputFormatChanged(formatHolder);\n    inputFormat = formatHolder.format;\n    eventDispatcher.inputFormatChanged(inputFormat);\n  }\n\n  @Override\n  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat)\n      throws ExoPlaybackException {\n    @C.Encoding int encoding;\n    MediaFormat mediaFormat;\n    if (passthroughMediaFormat != null) {\n      mediaFormat = passthroughMediaFormat;\n      encoding =\n          getPassthroughEncoding(\n              mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),\n              mediaFormat.getString(MediaFormat.KEY_MIME));\n    } else {\n      mediaFormat = outputMediaFormat;\n      encoding = getPcmEncoding(inputFormat);\n    }\n    int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);\n    int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);\n    int[] channelMap;\n    if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) {\n      channelMap = new int[inputFormat.channelCount];\n      for (int i = 0; i < inputFormat.channelCount; i++) {\n        channelMap[i] = i;\n      }\n    } else {\n      channelMap = null;\n    }\n\n    try {\n      audioSink.configure(\n          encoding,\n          channelCount,\n          sampleRate,\n          0,\n          channelMap,\n          inputFormat.encoderDelay,\n          inputFormat.encoderPadding);\n    } catch (AudioSink.ConfigurationException e) {\n      // TODO(internal: b/145658993) Use outputFormat instead.\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  /**\n   * Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link\n   * C#ENCODING_INVALID} if passthrough is not possible.\n   */\n  @C.Encoding\n  protected int getPassthroughEncoding(int channelCount, String mimeType) {\n    if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {\n      // E-AC3 JOC is object-based so the output channel count is arbitrary.\n      if (audioSink.supportsOutput(/* channelCount= */ Format.NO_VALUE, C.ENCODING_E_AC3_JOC)) {\n        return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC);\n      }\n      // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back.\n      mimeType = MimeTypes.AUDIO_E_AC3;\n    }\n\n    @C.Encoding int encoding = MimeTypes.getEncoding(mimeType);\n    if (audioSink.supportsOutput(channelCount, encoding)) {\n      return encoding;\n    } else {\n      return C.ENCODING_INVALID;\n    }\n  }\n\n  /**\n   * Called when the audio session id becomes known. The default implementation is a no-op. One\n   * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in\n   * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances\n   * should be released in {@link #onDisabled()} (if not before).\n   *\n   * @see AudioSink.Listener#onAudioSessionId(int)\n   */\n  protected void onAudioSessionId(int audioSessionId) {\n    // Do nothing.\n  }\n\n  /**\n   * @see AudioSink.Listener#onPositionDiscontinuity()\n   */\n  protected void onAudioTrackPositionDiscontinuity() {\n    // Do nothing.\n  }\n\n  /**\n   * @see AudioSink.Listener#onUnderrun(int, long, long)\n   */\n  protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,\n      long elapsedSinceLastFeedMs) {\n    // Do nothing.\n  }\n\n  @Override\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    super.onEnabled(joining);\n    eventDispatcher.enabled(decoderCounters);\n    int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;\n    if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {\n      audioSink.enableTunnelingV21(tunnelingAudioSessionId);\n    } else {\n      audioSink.disableTunneling();\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\n    super.onStreamChanged(formats, offsetUs);\n    if (lastInputTimeUs != C.TIME_UNSET) {\n      if (pendingStreamChangeCount == pendingStreamChangeTimesUs.length) {\n        Log.w(\n            TAG,\n            \"Too many stream changes, so dropping change at \"\n                + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1]);\n      } else {\n        pendingStreamChangeCount++;\n      }\n      pendingStreamChangeTimesUs[pendingStreamChangeCount - 1] = lastInputTimeUs;\n    }\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    super.onPositionReset(positionUs, joining);\n    audioSink.flush();\n    currentPositionUs = positionUs;\n    allowFirstBufferPositionDiscontinuity = true;\n    allowPositionDiscontinuity = true;\n    lastInputTimeUs = C.TIME_UNSET;\n    pendingStreamChangeCount = 0;\n  }\n\n  @Override\n  protected void onStarted() {\n    super.onStarted();\n    audioSink.play();\n  }\n\n  @Override\n  protected void onStopped() {\n    updateCurrentPosition();\n    audioSink.pause();\n    super.onStopped();\n  }\n\n  @Override\n  protected void onDisabled() {\n    try {\n      lastInputTimeUs = C.TIME_UNSET;\n      pendingStreamChangeCount = 0;\n      audioSink.flush();\n    } finally {\n      try {\n        super.onDisabled();\n      } finally {\n        eventDispatcher.disabled(decoderCounters);\n      }\n    }\n  }\n\n  @Override\n  protected void onReset() {\n    try {\n      super.onReset();\n    } finally {\n      audioSink.reset();\n    }\n  }\n\n  @Override\n  public boolean isEnded() {\n    return super.isEnded() && audioSink.isEnded();\n  }\n\n  @Override\n  public boolean isReady() {\n    return audioSink.hasPendingData() || super.isReady();\n  }\n\n  @Override\n  public long getPositionUs() {\n    if (getState() == STATE_STARTED) {\n      updateCurrentPosition();\n    }\n    return currentPositionUs;\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    audioSink.setPlaybackParameters(playbackParameters);\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return audioSink.getPlaybackParameters();\n  }\n\n  @Override\n  protected void onQueueInputBuffer(DecoderInputBuffer buffer) {\n    if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) {\n      // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314].\n      // Allow the position to jump if the first presentable input buffer has a timestamp that\n      // differs significantly from what was expected.\n      if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) {\n        currentPositionUs = buffer.timeUs;\n      }\n      allowFirstBufferPositionDiscontinuity = false;\n    }\n    lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs);\n  }\n\n  @CallSuper\n  @Override\n  protected void onProcessedOutputBuffer(long presentationTimeUs) {\n    while (pendingStreamChangeCount != 0 && presentationTimeUs >= pendingStreamChangeTimesUs[0]) {\n      audioSink.handleDiscontinuity();\n      pendingStreamChangeCount--;\n      System.arraycopy(\n          pendingStreamChangeTimesUs,\n          /* srcPos= */ 1,\n          pendingStreamChangeTimesUs,\n          /* destPos= */ 0,\n          pendingStreamChangeCount);\n    }\n  }\n\n  @Override\n  protected boolean processOutputBuffer(\n      long positionUs,\n      long elapsedRealtimeUs,\n      MediaCodec codec,\n      ByteBuffer buffer,\n      int bufferIndex,\n      int bufferFlags,\n      long bufferPresentationTimeUs,\n      boolean isDecodeOnlyBuffer,\n      boolean isLastBuffer,\n      Format format)\n      throws ExoPlaybackException {\n    if (codecNeedsEosBufferTimestampWorkaround\n        && bufferPresentationTimeUs == 0\n        && (bufferFlags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0\n        && lastInputTimeUs != C.TIME_UNSET) {\n      bufferPresentationTimeUs = lastInputTimeUs;\n    }\n\n    if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {\n      // Discard output buffers from the passthrough (raw) decoder containing codec specific data.\n      codec.releaseOutputBuffer(bufferIndex, false);\n      return true;\n    }\n\n    if (isDecodeOnlyBuffer) {\n      codec.releaseOutputBuffer(bufferIndex, false);\n      decoderCounters.skippedOutputBufferCount++;\n      audioSink.handleDiscontinuity();\n      return true;\n    }\n\n    try {\n      if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) {\n        codec.releaseOutputBuffer(bufferIndex, false);\n        decoderCounters.renderedOutputBufferCount++;\n        return true;\n      }\n    } catch (AudioSink.InitializationException | AudioSink.WriteException e) {\n      // TODO(internal: b/145658993) Use outputFormat instead.\n      throw createRendererException(e, inputFormat);\n    }\n    return false;\n  }\n\n  @Override\n  protected void renderToEndOfStream() throws ExoPlaybackException {\n    try {\n      audioSink.playToEndOfStream();\n    } catch (AudioSink.WriteException e) {\n      // TODO(internal: b/145658993) Use outputFormat instead.\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  @Override\n  public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {\n    switch (messageType) {\n      case C.MSG_SET_VOLUME:\n        audioSink.setVolume((Float) message);\n        break;\n      case C.MSG_SET_AUDIO_ATTRIBUTES:\n        AudioAttributes audioAttributes = (AudioAttributes) message;\n        audioSink.setAudioAttributes(audioAttributes);\n        break;\n      case C.MSG_SET_AUX_EFFECT_INFO:\n        AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;\n        audioSink.setAuxEffectInfo(auxEffectInfo);\n        break;\n      default:\n        super.handleMessage(messageType, message);\n        break;\n    }\n  }\n\n  /**\n   * Returns a maximum input size suitable for configuring a codec for {@code format} in a way that\n   * will allow possible adaptation to other compatible formats in {@code streamFormats}.\n   *\n   * @param codecInfo A {@link MediaCodecInfo} describing the decoder.\n   * @param format The {@link Format} for which the codec is being configured.\n   * @param streamFormats The possible stream formats.\n   * @return A suitable maximum input size.\n   */\n  protected int getCodecMaxInputSize(\n      MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {\n    int maxInputSize = getCodecMaxInputSize(codecInfo, format);\n    if (streamFormats.length == 1) {\n      // The single entry in streamFormats must correspond to the format for which the codec is\n      // being configured.\n      return maxInputSize;\n    }\n    for (Format streamFormat : streamFormats) {\n      if (codecInfo.isSeamlessAdaptationSupported(\n          format, streamFormat, /* isNewFormatComplete= */ false)) {\n        maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat));\n      }\n    }\n    return maxInputSize;\n  }\n\n  /**\n   * Returns a maximum input buffer size for a given {@link Format}.\n   *\n   * @param codecInfo A {@link MediaCodecInfo} describing the decoder.\n   * @param format The {@link Format}.\n   * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not\n   *     be determined.\n   */\n  private int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) {\n    if (\"OMX.google.raw.decoder\".equals(codecInfo.name)) {\n      // OMX.google.raw.decoder didn't resize its output buffers correctly prior to N, except on\n      // Android TV running M, so there's no point requesting a non-default input size. Doing so may\n      // cause a native crash, whereas not doing so will cause a more controlled failure when\n      // attempting to fill an input buffer. See: https://github.com/google/ExoPlayer/issues/4057.\n      if (Util.SDK_INT < 24 && !(Util.SDK_INT == 23 && Util.isTv(context))) {\n        return Format.NO_VALUE;\n      }\n    }\n    return format.maxInputSize;\n  }\n\n  /**\n   * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec}\n   * for decoding the given {@link Format} for playback.\n   *\n   * @param format The {@link Format} of the media.\n   * @param codecMimeType The MIME type handled by the codec.\n   * @param codecMaxInputSize The maximum input size supported by the codec.\n   * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if\n   *     no codec operating rate should be set.\n   * @return The framework {@link MediaFormat}.\n   */\n  @SuppressLint(\"InlinedApi\")\n  protected MediaFormat getMediaFormat(\n      Format format, String codecMimeType, int codecMaxInputSize, float codecOperatingRate) {\n    MediaFormat mediaFormat = new MediaFormat();\n    // Set format parameters that should always be set.\n    mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);\n    mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);\n    mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);\n    MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);\n    // Set codec max values.\n    MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxInputSize);\n    // Set codec configuration values.\n    if (Util.SDK_INT >= 23) {\n      mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);\n      if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) {\n        mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);\n      }\n    }\n    if (Util.SDK_INT <= 28 && MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) {\n      // On some older builds, the AC-4 decoder expects to receive samples formatted as raw frames\n      // not sync frames. Set a format key to override this.\n      mediaFormat.setInteger(\"ac4-is-sync\", 1);\n    }\n    return mediaFormat;\n  }\n\n  private void updateCurrentPosition() {\n    long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());\n    if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {\n      currentPositionUs =\n          allowPositionDiscontinuity\n              ? newCurrentPositionUs\n              : Math.max(currentPositionUs, newCurrentPositionUs);\n      allowPositionDiscontinuity = false;\n    }\n  }\n\n  /**\n   * Returns whether the device's decoders are known to not support setting the codec operating\n   * rate.\n   *\n   * <p>See <a href=\"https://github.com/google/ExoPlayer/issues/5821\">GitHub issue #5821</a>.\n   */\n  private static boolean deviceDoesntSupportOperatingRate() {\n    return Util.SDK_INT == 23\n        && (\"ZTE B2017G\".equals(Util.MODEL) || \"AXON 7 mini\".equals(Util.MODEL));\n  }\n\n  /**\n   * Returns whether the decoder is known to output six audio channels when provided with input with\n   * fewer than six channels.\n   * <p>\n   * See [Internal: b/35655036].\n   */\n  private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {\n    // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.\n    return Util.SDK_INT < 24 && \"OMX.SEC.aac.dec\".equals(codecName)\n        && \"samsung\".equals(Util.MANUFACTURER)\n        && (Util.DEVICE.startsWith(\"zeroflte\") || Util.DEVICE.startsWith(\"herolte\")\n        || Util.DEVICE.startsWith(\"heroqlte\"));\n  }\n\n  /**\n   * Returns whether the decoder may output a non-empty buffer with timestamp 0 as the end of stream\n   * buffer.\n   *\n   * <p>See <a href=\"https://github.com/google/ExoPlayer/issues/5045\">GitHub issue #5045</a>.\n   */\n  private static boolean codecNeedsEosBufferTimestampWorkaround(String codecName) {\n    return Util.SDK_INT < 21\n        && \"OMX.SEC.mp3.dec\".equals(codecName)\n        && \"samsung\".equals(Util.MANUFACTURER)\n        && (Util.DEVICE.startsWith(\"baffin\")\n            || Util.DEVICE.startsWith(\"grand\")\n            || Util.DEVICE.startsWith(\"fortuna\")\n            || Util.DEVICE.startsWith(\"gprimelte\")\n            || Util.DEVICE.startsWith(\"j2y18lte\")\n            || Util.DEVICE.startsWith(\"ms01\"));\n  }\n\n  @C.Encoding\n  private static int getPcmEncoding(Format format) {\n    // If the format is anything other than PCM then we assume that the audio decoder will output\n    // 16-bit PCM.\n    return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType)\n        ? format.pcmEncoding\n        : C.ENCODING_PCM_16BIT;\n  }\n\n  private final class AudioSinkListener implements AudioSink.Listener {\n\n    @Override\n    public void onAudioSessionId(int audioSessionId) {\n      eventDispatcher.audioSessionId(audioSessionId);\n      MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);\n    }\n\n    @Override\n    public void onPositionDiscontinuity() {\n      onAudioTrackPositionDiscontinuity();\n      // We are out of sync so allow currentPositionUs to jump backwards.\n      MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;\n    }\n\n    @Override\n    public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n      eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n      onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport java.nio.ByteBuffer;\n\n/**\n * An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit\n * integer PCM audio.\n */\n/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor {\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    @C.PcmEncoding int encoding = inputAudioFormat.encoding;\n    if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT\n        && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n    return encoding != C.ENCODING_PCM_16BIT\n        ? new AudioFormat(\n            inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT)\n        : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    // Prepare the output buffer.\n    int position = inputBuffer.position();\n    int limit = inputBuffer.limit();\n    int size = limit - position;\n    int resampledSize;\n    switch (inputAudioFormat.encoding) {\n      case C.ENCODING_PCM_8BIT:\n        resampledSize = size * 2;\n        break;\n      case C.ENCODING_PCM_24BIT:\n        resampledSize = (size / 3) * 2;\n        break;\n      case C.ENCODING_PCM_32BIT:\n        resampledSize = size / 2;\n        break;\n      case C.ENCODING_PCM_16BIT:\n      case C.ENCODING_PCM_FLOAT:\n      case C.ENCODING_PCM_A_LAW:\n      case C.ENCODING_PCM_MU_LAW:\n      case C.ENCODING_INVALID:\n      case Format.NO_VALUE:\n      default:\n        throw new IllegalStateException();\n    }\n\n    // Resample the little endian input and update the input/output buffers.\n    ByteBuffer buffer = replaceOutputBuffer(resampledSize);\n    switch (inputAudioFormat.encoding) {\n      case C.ENCODING_PCM_8BIT:\n        // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.\n        for (int i = position; i < limit; i++) {\n          buffer.put((byte) 0);\n          buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));\n        }\n        break;\n      case C.ENCODING_PCM_24BIT:\n        // 24->16 bit resampling. Drop the least significant byte.\n        for (int i = position; i < limit; i += 3) {\n          buffer.put(inputBuffer.get(i + 1));\n          buffer.put(inputBuffer.get(i + 2));\n        }\n        break;\n      case C.ENCODING_PCM_32BIT:\n        // 32->16 bit resampling. Drop the two least significant bytes.\n        for (int i = position; i < limit; i += 4) {\n          buffer.put(inputBuffer.get(i + 2));\n          buffer.put(inputBuffer.get(i + 3));\n        }\n        break;\n      case C.ENCODING_PCM_16BIT:\n      case C.ENCODING_PCM_FLOAT:\n      case C.ENCODING_PCM_A_LAW:\n      case C.ENCODING_PCM_MU_LAW:\n      case C.ENCODING_INVALID:\n      case Format.NO_VALUE:\n      default:\n        // Never happens.\n        throw new IllegalStateException();\n    }\n    inputBuffer.position(inputBuffer.limit());\n    buffer.flip();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\n\n/**\n * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit\n * PCM.\n */\npublic final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {\n\n  /**\n   * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify\n   * that part of audio as silent, in microseconds.\n   */\n  private static final long MINIMUM_SILENCE_DURATION_US = 150_000;\n  /**\n   * The duration of silence by which to extend non-silent sections, in microseconds. The value must\n   * not exceed {@link #MINIMUM_SILENCE_DURATION_US}.\n   */\n  private static final long PADDING_SILENCE_US = 20_000;\n  /**\n   * The absolute level below which an individual PCM sample is classified as silent. Note: the\n   * specified value will be rounded so that the threshold check only depends on the more\n   * significant byte, for efficiency.\n   */\n  private static final short SILENCE_THRESHOLD_LEVEL = 1024;\n\n  /**\n   * Threshold for classifying an individual PCM sample as silent based on its more significant\n   * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding.\n   */\n  private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8;\n\n  /** Trimming states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    STATE_NOISY,\n    STATE_MAYBE_SILENT,\n    STATE_SILENT,\n  })\n  private @interface State {}\n  /** State when the input is not silent. */\n  private static final int STATE_NOISY = 0;\n  /** State when the input may be silent but we haven't read enough yet to know. */\n  private static final int STATE_MAYBE_SILENT = 1;\n  /** State when the input is silent. */\n  private static final int STATE_SILENT = 2;\n\n  private int bytesPerFrame;\n\n  private boolean enabled;\n\n  /**\n   * Buffers audio data that may be classified as silence while in {@link #STATE_MAYBE_SILENT}. If\n   * the input becomes noisy before the buffer has filled, it will be output. Otherwise, the buffer\n   * contents will be dropped and the state will transition to {@link #STATE_SILENT}.\n   */\n  private byte[] maybeSilenceBuffer;\n\n  /**\n   * Stores the latest part of the input while silent. It will be output as padding if the next\n   * input is noisy.\n   */\n  private byte[] paddingBuffer;\n\n  @State private int state;\n  private int maybeSilenceBufferSize;\n  private int paddingSize;\n  private boolean hasOutputNoise;\n  private long skippedFrames;\n\n  /** Creates a new silence trimming audio processor. */\n  public SilenceSkippingAudioProcessor() {\n    maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;\n    paddingBuffer = Util.EMPTY_BYTE_ARRAY;\n  }\n\n  /**\n   * Sets whether to skip silence in the input. This method may only be called after draining data\n   * through the processor. The value returned by {@link #isActive()} may change, and the processor\n   * must be {@link #flush() flushed} before queueing more data.\n   *\n   * @param enabled Whether to skip silence in the input.\n   */\n  public void setEnabled(boolean enabled) {\n    this.enabled = enabled;\n  }\n\n  /**\n   * Returns the total number of frames of input audio that were skipped due to being classified as\n   * silence since the last call to {@link #flush()}.\n   */\n  public long getSkippedFrames() {\n    return skippedFrames;\n  }\n\n  // AudioProcessor implementation.\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n    return enabled ? inputAudioFormat : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public boolean isActive() {\n    return enabled;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    while (inputBuffer.hasRemaining() && !hasPendingOutput()) {\n      switch (state) {\n        case STATE_NOISY:\n          processNoisy(inputBuffer);\n          break;\n        case STATE_MAYBE_SILENT:\n          processMaybeSilence(inputBuffer);\n          break;\n        case STATE_SILENT:\n          processSilence(inputBuffer);\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  protected void onQueueEndOfStream() {\n    if (maybeSilenceBufferSize > 0) {\n      // We haven't received enough silence to transition to the silent state, so output the buffer.\n      output(maybeSilenceBuffer, maybeSilenceBufferSize);\n    }\n    if (!hasOutputNoise) {\n      skippedFrames += paddingSize / bytesPerFrame;\n    }\n  }\n\n  @Override\n  protected void onFlush() {\n    if (enabled) {\n      bytesPerFrame = inputAudioFormat.bytesPerFrame;\n      int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame;\n      if (maybeSilenceBuffer.length != maybeSilenceBufferSize) {\n        maybeSilenceBuffer = new byte[maybeSilenceBufferSize];\n      }\n      paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame;\n      if (paddingBuffer.length != paddingSize) {\n        paddingBuffer = new byte[paddingSize];\n      }\n    }\n    state = STATE_NOISY;\n    skippedFrames = 0;\n    maybeSilenceBufferSize = 0;\n    hasOutputNoise = false;\n  }\n\n  @Override\n  protected void onReset() {\n    enabled = false;\n    paddingSize = 0;\n    maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;\n    paddingBuffer = Util.EMPTY_BYTE_ARRAY;\n  }\n\n  // Internal methods.\n\n  /**\n   * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_NOISY},\n   * updating the state if needed.\n   */\n  private void processNoisy(ByteBuffer inputBuffer) {\n    int limit = inputBuffer.limit();\n\n    // Check if there's any noise within the maybe silence buffer duration.\n    inputBuffer.limit(Math.min(limit, inputBuffer.position() + maybeSilenceBuffer.length));\n    int noiseLimit = findNoiseLimit(inputBuffer);\n    if (noiseLimit == inputBuffer.position()) {\n      // The buffer contains the start of possible silence.\n      state = STATE_MAYBE_SILENT;\n    } else {\n      inputBuffer.limit(noiseLimit);\n      output(inputBuffer);\n    }\n\n    // Restore the limit.\n    inputBuffer.limit(limit);\n  }\n\n  /**\n   * Incrementally processes new input from {@code inputBuffer} while in {@link\n   * #STATE_MAYBE_SILENT}, updating the state if needed.\n   */\n  private void processMaybeSilence(ByteBuffer inputBuffer) {\n    int limit = inputBuffer.limit();\n    int noisePosition = findNoisePosition(inputBuffer);\n    int maybeSilenceInputSize = noisePosition - inputBuffer.position();\n    int maybeSilenceBufferRemaining = maybeSilenceBuffer.length - maybeSilenceBufferSize;\n    if (noisePosition < limit && maybeSilenceInputSize < maybeSilenceBufferRemaining) {\n      // The maybe silence buffer isn't full, so output it and switch back to the noisy state.\n      output(maybeSilenceBuffer, maybeSilenceBufferSize);\n      maybeSilenceBufferSize = 0;\n      state = STATE_NOISY;\n    } else {\n      // Fill as much of the maybe silence buffer as possible.\n      int bytesToWrite = Math.min(maybeSilenceInputSize, maybeSilenceBufferRemaining);\n      inputBuffer.limit(inputBuffer.position() + bytesToWrite);\n      inputBuffer.get(maybeSilenceBuffer, maybeSilenceBufferSize, bytesToWrite);\n      maybeSilenceBufferSize += bytesToWrite;\n      if (maybeSilenceBufferSize == maybeSilenceBuffer.length) {\n        // We've reached a period of silence, so skip it, taking in to account padding for both\n        // the noisy to silent transition and any future silent to noisy transition.\n        if (hasOutputNoise) {\n          output(maybeSilenceBuffer, paddingSize);\n          skippedFrames += (maybeSilenceBufferSize - paddingSize * 2) / bytesPerFrame;\n        } else {\n          skippedFrames += (maybeSilenceBufferSize - paddingSize) / bytesPerFrame;\n        }\n        updatePaddingBuffer(inputBuffer, maybeSilenceBuffer, maybeSilenceBufferSize);\n        maybeSilenceBufferSize = 0;\n        state = STATE_SILENT;\n      }\n\n      // Restore the limit.\n      inputBuffer.limit(limit);\n    }\n  }\n\n  /**\n   * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_SILENT},\n   * updating the state if needed.\n   */\n  private void processSilence(ByteBuffer inputBuffer) {\n    int limit = inputBuffer.limit();\n    int noisyPosition = findNoisePosition(inputBuffer);\n    inputBuffer.limit(noisyPosition);\n    skippedFrames += inputBuffer.remaining() / bytesPerFrame;\n    updatePaddingBuffer(inputBuffer, paddingBuffer, paddingSize);\n    if (noisyPosition < limit) {\n      // Output the padding, which may include previous input as well as new input, then transition\n      // back to the noisy state.\n      output(paddingBuffer, paddingSize);\n      state = STATE_NOISY;\n\n      // Restore the limit.\n      inputBuffer.limit(limit);\n    }\n  }\n\n  /**\n   * Copies {@code length} elements from {@code data} to populate a new output buffer from the\n   * processor.\n   */\n  private void output(byte[] data, int length) {\n    replaceOutputBuffer(length).put(data, 0, length).flip();\n    if (length > 0) {\n      hasOutputNoise = true;\n    }\n  }\n\n  /**\n   * Copies remaining bytes from {@code data} to populate a new output buffer from the processor.\n   */\n  private void output(ByteBuffer data) {\n    int length = data.remaining();\n    replaceOutputBuffer(length).put(data).flip();\n    if (length > 0) {\n      hasOutputNoise = true;\n    }\n  }\n\n  /**\n   * Fills {@link #paddingBuffer} using data from {@code input}, plus any additional buffered data\n   * at the end of {@code buffer} (up to its {@code size}) required to fill it, advancing the input\n   * position.\n   */\n  private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) {\n    int fromInputSize = Math.min(input.remaining(), paddingSize);\n    int fromBufferSize = paddingSize - fromInputSize;\n    System.arraycopy(\n        /* src= */ buffer,\n        /* srcPos= */ size - fromBufferSize,\n        /* dest= */ paddingBuffer,\n        /* destPos= */ 0,\n        /* length= */ fromBufferSize);\n    input.position(input.limit() - fromInputSize);\n    input.get(paddingBuffer, fromBufferSize, fromInputSize);\n  }\n\n  /**\n   * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio.\n   */\n  private int durationUsToFrames(long durationUs) {\n    return (int) ((durationUs * inputAudioFormat.sampleRate) / C.MICROS_PER_SECOND);\n  }\n\n  /**\n   * Returns the earliest byte position in [position, limit) of {@code buffer} that contains a frame\n   * classified as a noisy frame, or the limit of the buffer if no such frame exists.\n   */\n  private int findNoisePosition(ByteBuffer buffer) {\n    // The input is in ByteOrder.nativeOrder(), which is little endian on Android.\n    for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) {\n      if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {\n        // Round to the start of the frame.\n        return bytesPerFrame * (i / bytesPerFrame);\n      }\n    }\n    return buffer.limit();\n  }\n\n  /**\n   * Returns the earliest byte position in [position, limit) of {@code buffer} such that all frames\n   * from the byte position to the limit are classified as silent.\n   */\n  private int findNoiseLimit(ByteBuffer buffer) {\n    // The input is in ByteOrder.nativeOrder(), which is little endian on Android.\n    for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) {\n      if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {\n        // Return the start of the next frame.\n        return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame;\n      }\n    }\n    return buffer.position();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport android.media.audiofx.Virtualizer;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.decoder.SimpleDecoder;\nimport com.google.android.exoplayer2.decoder.SimpleOutputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Decodes and renders audio using a {@link SimpleDecoder}.\n *\n * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}\n * on the playback thread:\n *\n * <ul>\n *   <li>Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be\n *       a {@link Float} with 0 being silence and 1 being unity gain.\n *   <li>Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The\n *       message payload should be an {@link AudioAttributes}\n *       instance that will configure the underlying audio track.\n *   <li>Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The\n *       message payload should be an {@link AuxEffectInfo} instance that will configure the\n *       underlying audio track.\n * </ul>\n */\npublic abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    REINITIALIZATION_STATE_NONE,\n    REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,\n    REINITIALIZATION_STATE_WAIT_END_OF_STREAM\n  })\n  private @interface ReinitializationState {}\n  /**\n   * The decoder does not need to be re-initialized.\n   */\n  private static final int REINITIALIZATION_STATE_NONE = 0;\n  /**\n   * The input format has changed in a way that requires the decoder to be re-initialized, but we\n   * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to\n   * ensure that it outputs any remaining buffers before we release it.\n   */\n  private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;\n  /**\n   * The input format has changed in a way that requires the decoder to be re-initialized, and we've\n   * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an\n   * end of stream signal to indicate that it has output any remaining buffers before we release it.\n   */\n  private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;\n\n  private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;\n  private final boolean playClearSamplesWithoutKeys;\n  private final EventDispatcher eventDispatcher;\n  private final AudioSink audioSink;\n  private final DecoderInputBuffer flagsOnlyBuffer;\n\n  private DecoderCounters decoderCounters;\n  private Format inputFormat;\n  private int encoderDelay;\n  private int encoderPadding;\n  private SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,\n        ? extends AudioDecoderException> decoder;\n  private DecoderInputBuffer inputBuffer;\n  private SimpleOutputBuffer outputBuffer;\n  @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;\n  @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;\n\n  @ReinitializationState private int decoderReinitializationState;\n  private boolean decoderReceivedBuffers;\n  private boolean audioTrackNeedsConfigure;\n\n  private long currentPositionUs;\n  private boolean allowFirstBufferPositionDiscontinuity;\n  private boolean allowPositionDiscontinuity;\n  private boolean inputStreamEnded;\n  private boolean outputStreamEnded;\n  private boolean waitingForKeys;\n\n  public SimpleDecoderAudioRenderer() {\n    this(/* eventHandler= */ null, /* eventListener= */ null);\n  }\n\n  /**\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.\n   */\n  public SimpleDecoderAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioProcessor... audioProcessors) {\n    this(\n        eventHandler,\n        eventListener,\n        /* audioCapabilities= */ null,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        audioProcessors);\n  }\n\n  /**\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   */\n  public SimpleDecoderAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      @Nullable AudioCapabilities audioCapabilities) {\n    this(\n        eventHandler,\n        eventListener,\n        audioCapabilities,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false);\n  }\n\n  /**\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param audioCapabilities The audio capabilities for playback on this device. May be null if the\n   *     default capabilities (no encoded audio passthrough support) should be assumed.\n   * @param drmSessionManager For use with encrypted media. May be null if support for encrypted\n   *     media is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.\n   */\n  public SimpleDecoderAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      @Nullable AudioCapabilities audioCapabilities,\n      @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      AudioProcessor... audioProcessors) {\n    this(eventHandler, eventListener, drmSessionManager,\n        playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors));\n  }\n\n  /**\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param drmSessionManager For use with encrypted media. May be null if support for encrypted\n   *     media is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param audioSink The sink to which audio will be output.\n   */\n  public SimpleDecoderAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      AudioSink audioSink) {\n    super(C.TRACK_TYPE_AUDIO);\n    this.drmSessionManager = drmSessionManager;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    eventDispatcher = new EventDispatcher(eventHandler, eventListener);\n    this.audioSink = audioSink;\n    audioSink.setListener(new AudioSinkListener());\n    flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n    audioTrackNeedsConfigure = true;\n  }\n\n  @Override\n  @Nullable\n  public MediaClock getMediaClock() {\n    return this;\n  }\n\n  @Override\n  @Capabilities\n  public final int supportsFormat(Format format) {\n    if (!MimeTypes.isAudio(format.sampleMimeType)) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n    }\n    @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format);\n    if (formatSupport <= FORMAT_UNSUPPORTED_DRM) {\n      return RendererCapabilities.create(formatSupport);\n    }\n    @TunnelingSupport\n    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;\n    return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport);\n  }\n\n  /**\n   * Returns the {@link FormatSupport} for the given {@link Format}.\n   *\n   * @param drmSessionManager The renderer's {@link DrmSessionManager}.\n   * @param format The format, which has an audio {@link Format#sampleMimeType}.\n   * @return The {@link FormatSupport} for this {@link Format}.\n   */\n  @FormatSupport\n  protected abstract int supportsFormatInternal(\n      @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format);\n\n  /**\n   * Returns whether the sink supports the audio format.\n   *\n   * @see AudioSink#supportsOutput(int, int)\n   */\n  protected final boolean supportsOutput(int channelCount, @C.Encoding int encoding) {\n    return audioSink.supportsOutput(channelCount, encoding);\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (outputStreamEnded) {\n      try {\n        audioSink.playToEndOfStream();\n      } catch (AudioSink.WriteException e) {\n        throw createRendererException(e, inputFormat);\n      }\n      return;\n    }\n\n    // Try and read a format if we don't have one already.\n    if (inputFormat == null) {\n      // We don't have a format yet, so try and read one.\n      FormatHolder formatHolder = getFormatHolder();\n      flagsOnlyBuffer.clear();\n      int result = readSource(formatHolder, flagsOnlyBuffer, true);\n      if (result == C.RESULT_FORMAT_READ) {\n        onInputFormatChanged(formatHolder);\n      } else if (result == C.RESULT_BUFFER_READ) {\n        // End of stream read having not read a format.\n        Assertions.checkState(flagsOnlyBuffer.isEndOfStream());\n        inputStreamEnded = true;\n        processEndOfStream();\n        return;\n      } else {\n        // We still don't have a format and can't make progress without one.\n        return;\n      }\n    }\n\n    // If we don't have a decoder yet, we need to instantiate one.\n    maybeInitDecoder();\n\n    if (decoder != null) {\n      try {\n        // Rendering loop.\n        TraceUtil.beginSection(\"drainAndFeed\");\n        while (drainOutputBuffer()) {}\n        while (feedInputBuffer()) {}\n        TraceUtil.endSection();\n      } catch (AudioDecoderException | AudioSink.ConfigurationException\n          | AudioSink.InitializationException | AudioSink.WriteException e) {\n        throw createRendererException(e, inputFormat);\n      }\n      decoderCounters.ensureUpdated();\n    }\n  }\n\n  /**\n   * Called when the audio session id becomes known. The default implementation is a no-op. One\n   * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in\n   * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances\n   * should be released in {@link #onDisabled()} (if not before).\n   *\n   * @see AudioSink.Listener#onAudioSessionId(int)\n   */\n  protected void onAudioSessionId(int audioSessionId) {\n    // Do nothing.\n  }\n\n  /**\n   * @see AudioSink.Listener#onPositionDiscontinuity()\n   */\n  protected void onAudioTrackPositionDiscontinuity() {\n    // Do nothing.\n  }\n\n  /**\n   * @see AudioSink.Listener#onUnderrun(int, long, long)\n   */\n  protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,\n      long elapsedSinceLastFeedMs) {\n    // Do nothing.\n  }\n\n  /**\n   * Creates a decoder for the given format.\n   *\n   * @param format The format for which a decoder is required.\n   * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.\n   *     Maybe null and can be ignored if decoder does not handle encrypted content.\n   * @return The decoder.\n   * @throws AudioDecoderException If an error occurred creating a suitable decoder.\n   */\n  protected abstract SimpleDecoder<\n          DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException>\n      createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)\n          throws AudioDecoderException;\n\n  /**\n   * Returns the format of audio buffers output by the decoder. Will not be called until the first\n   * output buffer has been dequeued, so the decoder may use input data to determine the format.\n   * <p>\n   * The default implementation returns a 16-bit PCM format with the same channel count and sample\n   * rate as the input.\n   */\n  protected Format getOutputFormat() {\n    return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,\n        Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT,\n        null, null, 0, null);\n  }\n\n  /**\n   * Returns whether the existing decoder can be kept for a new format.\n   *\n   * @param oldFormat The previous format.\n   * @param newFormat The new format.\n   * @return True if the existing decoder can be kept.\n   */\n  protected boolean canKeepCodec(Format oldFormat, Format newFormat) {\n    return false;\n  }\n\n  private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,\n      AudioSink.ConfigurationException, AudioSink.InitializationException,\n      AudioSink.WriteException {\n    if (outputBuffer == null) {\n      outputBuffer = decoder.dequeueOutputBuffer();\n      if (outputBuffer == null) {\n        return false;\n      }\n      if (outputBuffer.skippedOutputBufferCount > 0) {\n        decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;\n        audioSink.handleDiscontinuity();\n      }\n    }\n\n    if (outputBuffer.isEndOfStream()) {\n      if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {\n        // We're waiting to re-initialize the decoder, and have now processed all final buffers.\n        releaseDecoder();\n        maybeInitDecoder();\n        // The audio track may need to be recreated once the new output format is known.\n        audioTrackNeedsConfigure = true;\n      } else {\n        outputBuffer.release();\n        outputBuffer = null;\n        processEndOfStream();\n      }\n      return false;\n    }\n\n    if (audioTrackNeedsConfigure) {\n      Format outputFormat = getOutputFormat();\n      audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,\n          outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);\n      audioTrackNeedsConfigure = false;\n    }\n\n    if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {\n      decoderCounters.renderedOutputBufferCount++;\n      outputBuffer.release();\n      outputBuffer = null;\n      return true;\n    }\n\n    return false;\n  }\n\n  private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException {\n    if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM\n        || inputStreamEnded) {\n      // We need to reinitialize the decoder or the input stream has ended.\n      return false;\n    }\n\n    if (inputBuffer == null) {\n      inputBuffer = decoder.dequeueInputBuffer();\n      if (inputBuffer == null) {\n        return false;\n      }\n    }\n\n    if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {\n      inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n      decoder.queueInputBuffer(inputBuffer);\n      inputBuffer = null;\n      decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;\n      return false;\n    }\n\n    int result;\n    FormatHolder formatHolder = getFormatHolder();\n    if (waitingForKeys) {\n      // We've already read an encrypted sample into buffer, and are waiting for keys.\n      result = C.RESULT_BUFFER_READ;\n    } else {\n      result = readSource(formatHolder, inputBuffer, false);\n    }\n\n    if (result == C.RESULT_NOTHING_READ) {\n      return false;\n    }\n    if (result == C.RESULT_FORMAT_READ) {\n      onInputFormatChanged(formatHolder);\n      return true;\n    }\n    if (inputBuffer.isEndOfStream()) {\n      inputStreamEnded = true;\n      decoder.queueInputBuffer(inputBuffer);\n      inputBuffer = null;\n      return false;\n    }\n    boolean bufferEncrypted = inputBuffer.isEncrypted();\n    waitingForKeys = shouldWaitForKeys(bufferEncrypted);\n    if (waitingForKeys) {\n      return false;\n    }\n    inputBuffer.flip();\n    onQueueInputBuffer(inputBuffer);\n    decoder.queueInputBuffer(inputBuffer);\n    decoderReceivedBuffers = true;\n    decoderCounters.inputBufferCount++;\n    inputBuffer = null;\n    return true;\n  }\n\n  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {\n    if (decoderDrmSession == null\n        || (!bufferEncrypted\n            && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = decoderDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw createRendererException(decoderDrmSession.getError(), inputFormat);\n    }\n    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;\n  }\n\n  private void processEndOfStream() throws ExoPlaybackException {\n    outputStreamEnded = true;\n    try {\n      audioSink.playToEndOfStream();\n    } catch (AudioSink.WriteException e) {\n      // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer.\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  private void flushDecoder() throws ExoPlaybackException {\n    waitingForKeys = false;\n    if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {\n      releaseDecoder();\n      maybeInitDecoder();\n    } else {\n      inputBuffer = null;\n      if (outputBuffer != null) {\n        outputBuffer.release();\n        outputBuffer = null;\n      }\n      decoder.flush();\n      decoderReceivedBuffers = false;\n    }\n  }\n\n  @Override\n  public boolean isEnded() {\n    return outputStreamEnded && audioSink.isEnded();\n  }\n\n  @Override\n  public boolean isReady() {\n    return audioSink.hasPendingData()\n        || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));\n  }\n\n  @Override\n  public long getPositionUs() {\n    if (getState() == STATE_STARTED) {\n      updateCurrentPosition();\n    }\n    return currentPositionUs;\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    audioSink.setPlaybackParameters(playbackParameters);\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return audioSink.getPlaybackParameters();\n  }\n\n  @Override\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    decoderCounters = new DecoderCounters();\n    eventDispatcher.enabled(decoderCounters);\n    int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;\n    if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {\n      audioSink.enableTunnelingV21(tunnelingAudioSessionId);\n    } else {\n      audioSink.disableTunneling();\n    }\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    audioSink.flush();\n    currentPositionUs = positionUs;\n    allowFirstBufferPositionDiscontinuity = true;\n    allowPositionDiscontinuity = true;\n    inputStreamEnded = false;\n    outputStreamEnded = false;\n    if (decoder != null) {\n      flushDecoder();\n    }\n  }\n\n  @Override\n  protected void onStarted() {\n    audioSink.play();\n  }\n\n  @Override\n  protected void onStopped() {\n    updateCurrentPosition();\n    audioSink.pause();\n  }\n\n  @Override\n  protected void onDisabled() {\n    inputFormat = null;\n    audioTrackNeedsConfigure = true;\n    waitingForKeys = false;\n    try {\n      setSourceDrmSession(null);\n      releaseDecoder();\n      audioSink.reset();\n    } finally {\n      eventDispatcher.disabled(decoderCounters);\n    }\n  }\n\n  @Override\n  public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {\n    switch (messageType) {\n      case C.MSG_SET_VOLUME:\n        audioSink.setVolume((Float) message);\n        break;\n      case C.MSG_SET_AUDIO_ATTRIBUTES:\n        AudioAttributes audioAttributes = (AudioAttributes) message;\n        audioSink.setAudioAttributes(audioAttributes);\n        break;\n      case C.MSG_SET_AUX_EFFECT_INFO:\n        AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;\n        audioSink.setAuxEffectInfo(auxEffectInfo);\n        break;\n      default:\n        super.handleMessage(messageType, message);\n        break;\n    }\n  }\n\n  private void maybeInitDecoder() throws ExoPlaybackException {\n    if (decoder != null) {\n      return;\n    }\n\n    setDecoderDrmSession(sourceDrmSession);\n\n    ExoMediaCrypto mediaCrypto = null;\n    if (decoderDrmSession != null) {\n      mediaCrypto = decoderDrmSession.getMediaCrypto();\n      if (mediaCrypto == null) {\n        DrmSessionException drmError = decoderDrmSession.getError();\n        if (drmError != null) {\n          // Continue for now. We may be able to avoid failure if the session recovers, or if a new\n          // input format causes the session to be replaced before it's used.\n        } else {\n          // The drm session isn't open yet.\n          return;\n        }\n      }\n    }\n\n    try {\n      long codecInitializingTimestamp = SystemClock.elapsedRealtime();\n      TraceUtil.beginSection(\"createAudioDecoder\");\n      decoder = createDecoder(inputFormat, mediaCrypto);\n      TraceUtil.endSection();\n      long codecInitializedTimestamp = SystemClock.elapsedRealtime();\n      eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,\n          codecInitializedTimestamp - codecInitializingTimestamp);\n      decoderCounters.decoderInitCount++;\n    } catch (AudioDecoderException e) {\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  private void releaseDecoder() {\n    inputBuffer = null;\n    outputBuffer = null;\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n    decoderReceivedBuffers = false;\n    if (decoder != null) {\n      decoder.release();\n      decoder = null;\n      decoderCounters.decoderReleaseCount++;\n    }\n    setDecoderDrmSession(null);\n  }\n\n  private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession.replaceSession(sourceDrmSession, session);\n    sourceDrmSession = session;\n  }\n\n  private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession.replaceSession(decoderDrmSession, session);\n    decoderDrmSession = session;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {\n    Format newFormat = Assertions.checkNotNull(formatHolder.format);\n    if (formatHolder.includesDrmSession) {\n      setSourceDrmSession((DrmSession<ExoMediaCrypto>) formatHolder.drmSession);\n    } else {\n      sourceDrmSession =\n          getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);\n    }\n    Format oldFormat = inputFormat;\n    inputFormat = newFormat;\n\n    if (!canKeepCodec(oldFormat, inputFormat)) {\n      if (decoderReceivedBuffers) {\n        // Signal end of stream and wait for any final output buffers before re-initialization.\n        decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;\n      } else {\n        // There aren't any final output buffers, so release the decoder immediately.\n        releaseDecoder();\n        maybeInitDecoder();\n        audioTrackNeedsConfigure = true;\n      }\n    }\n\n    encoderDelay = inputFormat.encoderDelay;\n    encoderPadding = inputFormat.encoderPadding;\n\n    eventDispatcher.inputFormatChanged(inputFormat);\n  }\n\n  private void onQueueInputBuffer(DecoderInputBuffer buffer) {\n    if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) {\n      // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314].\n      // Allow the position to jump if the first presentable input buffer has a timestamp that\n      // differs significantly from what was expected.\n      if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) {\n        currentPositionUs = buffer.timeUs;\n      }\n      allowFirstBufferPositionDiscontinuity = false;\n    }\n  }\n\n  private void updateCurrentPosition() {\n    long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());\n    if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {\n      currentPositionUs =\n          allowPositionDiscontinuity\n              ? newCurrentPositionUs\n              : Math.max(currentPositionUs, newCurrentPositionUs);\n      allowPositionDiscontinuity = false;\n    }\n  }\n\n  private final class AudioSinkListener implements AudioSink.Listener {\n\n    @Override\n    public void onAudioSessionId(int audioSessionId) {\n      eventDispatcher.audioSessionId(audioSessionId);\n      SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);\n    }\n\n    @Override\n    public void onPositionDiscontinuity() {\n      onAudioTrackPositionDiscontinuity();\n      // We are out of sync so allow currentPositionUs to jump backwards.\n      SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;\n    }\n\n    @Override\n    public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n      eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n      onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/Sonic.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\n * Copyright (C) 2010 Bill Cox, Sonic Library\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.nio.ShortBuffer;\nimport java.util.Arrays;\n\n/**\n * Sonic audio stream processor for time/pitch stretching.\n * <p>\n * Based on https://github.com/waywardgeek/sonic.\n */\n/* package */ final class Sonic {\n\n  private static final int MINIMUM_PITCH = 65;\n  private static final int MAXIMUM_PITCH = 400;\n  private static final int AMDF_FREQUENCY = 4000;\n  private static final int BYTES_PER_SAMPLE = 2;\n\n  private final int inputSampleRateHz;\n  private final int channelCount;\n  private final float speed;\n  private final float pitch;\n  private final float rate;\n  private final int minPeriod;\n  private final int maxPeriod;\n  private final int maxRequiredFrameCount;\n  private final short[] downSampleBuffer;\n\n  private short[] inputBuffer;\n  private int inputFrameCount;\n  private short[] outputBuffer;\n  private int outputFrameCount;\n  private short[] pitchBuffer;\n  private int pitchFrameCount;\n  private int oldRatePosition;\n  private int newRatePosition;\n  private int remainingInputToCopyFrameCount;\n  private int prevPeriod;\n  private int prevMinDiff;\n  private int minDiff;\n  private int maxDiff;\n\n  /**\n   * Creates a new Sonic audio stream processor.\n   *\n   * @param inputSampleRateHz The sample rate of input audio, in hertz.\n   * @param channelCount The number of channels in the input audio.\n   * @param speed The speedup factor for output audio.\n   * @param pitch The pitch factor for output audio.\n   * @param outputSampleRateHz The sample rate for output audio, in hertz.\n   */\n  public Sonic(\n      int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) {\n    this.inputSampleRateHz = inputSampleRateHz;\n    this.channelCount = channelCount;\n    this.speed = speed;\n    this.pitch = pitch;\n    rate = (float) inputSampleRateHz / outputSampleRateHz;\n    minPeriod = inputSampleRateHz / MAXIMUM_PITCH;\n    maxPeriod = inputSampleRateHz / MINIMUM_PITCH;\n    maxRequiredFrameCount = 2 * maxPeriod;\n    downSampleBuffer = new short[maxRequiredFrameCount];\n    inputBuffer = new short[maxRequiredFrameCount * channelCount];\n    outputBuffer = new short[maxRequiredFrameCount * channelCount];\n    pitchBuffer = new short[maxRequiredFrameCount * channelCount];\n  }\n\n  /**\n   * Queues remaining data from {@code buffer}, and advances its position by the number of bytes\n   * consumed.\n   *\n   * @param buffer A {@link ShortBuffer} containing input data between its position and limit.\n   */\n  public void queueInput(ShortBuffer buffer) {\n    int framesToWrite = buffer.remaining() / channelCount;\n    int bytesToWrite = framesToWrite * channelCount * 2;\n    inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite);\n    buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2);\n    inputFrameCount += framesToWrite;\n    processStreamInput();\n  }\n\n  /**\n   * Gets available output, outputting to the start of {@code buffer}. The buffer's position will be\n   * advanced by the number of bytes written.\n   *\n   * @param buffer A {@link ShortBuffer} into which output will be written.\n   */\n  public void getOutput(ShortBuffer buffer) {\n    int framesToRead = Math.min(buffer.remaining() / channelCount, outputFrameCount);\n    buffer.put(outputBuffer, 0, framesToRead * channelCount);\n    outputFrameCount -= framesToRead;\n    System.arraycopy(\n        outputBuffer,\n        framesToRead * channelCount,\n        outputBuffer,\n        0,\n        outputFrameCount * channelCount);\n  }\n\n  /**\n   * Forces generating output using whatever data has been queued already. No extra delay will be\n   * added to the output, but flushing in the middle of words could introduce distortion.\n   */\n  public void queueEndOfStream() {\n    int remainingFrameCount = inputFrameCount;\n    float s = speed / pitch;\n    float r = rate * pitch;\n    int expectedOutputFrames =\n        outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f);\n\n    // Add enough silence to flush both input and pitch buffers.\n    inputBuffer =\n        ensureSpaceForAdditionalFrames(\n            inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount);\n    for (int xSample = 0; xSample < 2 * maxRequiredFrameCount * channelCount; xSample++) {\n      inputBuffer[remainingFrameCount * channelCount + xSample] = 0;\n    }\n    inputFrameCount += 2 * maxRequiredFrameCount;\n    processStreamInput();\n    // Throw away any extra frames we generated due to the silence we added.\n    if (outputFrameCount > expectedOutputFrames) {\n      outputFrameCount = expectedOutputFrames;\n    }\n    // Empty input and pitch buffers.\n    inputFrameCount = 0;\n    remainingInputToCopyFrameCount = 0;\n    pitchFrameCount = 0;\n  }\n\n  /** Clears state in preparation for receiving a new stream of input buffers. */\n  public void flush() {\n    inputFrameCount = 0;\n    outputFrameCount = 0;\n    pitchFrameCount = 0;\n    oldRatePosition = 0;\n    newRatePosition = 0;\n    remainingInputToCopyFrameCount = 0;\n    prevPeriod = 0;\n    prevMinDiff = 0;\n    minDiff = 0;\n    maxDiff = 0;\n  }\n\n  /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */\n  public int getOutputSize() {\n    return outputFrameCount * channelCount * BYTES_PER_SAMPLE;\n  }\n\n  // Internal methods.\n\n  /**\n   * Returns {@code buffer} or a copy of it, such that there is enough space in the returned buffer\n   * to store {@code newFrameCount} additional frames.\n   *\n   * @param buffer The buffer.\n   * @param frameCount The number of frames already in the buffer.\n   * @param additionalFrameCount The number of additional frames that need to be stored in the\n   *     buffer.\n   * @return A buffer with enough space for the additional frames.\n   */\n  private short[] ensureSpaceForAdditionalFrames(\n      short[] buffer, int frameCount, int additionalFrameCount) {\n    int currentCapacityFrames = buffer.length / channelCount;\n    if (frameCount + additionalFrameCount <= currentCapacityFrames) {\n      return buffer;\n    } else {\n      int newCapacityFrames = 3 * currentCapacityFrames / 2 + additionalFrameCount;\n      return Arrays.copyOf(buffer, newCapacityFrames * channelCount);\n    }\n  }\n\n  private void removeProcessedInputFrames(int positionFrames) {\n    int remainingFrames = inputFrameCount - positionFrames;\n    System.arraycopy(\n        inputBuffer, positionFrames * channelCount, inputBuffer, 0, remainingFrames * channelCount);\n    inputFrameCount = remainingFrames;\n  }\n\n  private void copyToOutput(short[] samples, int positionFrames, int frameCount) {\n    outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, frameCount);\n    System.arraycopy(\n        samples,\n        positionFrames * channelCount,\n        outputBuffer,\n        outputFrameCount * channelCount,\n        frameCount * channelCount);\n    outputFrameCount += frameCount;\n  }\n\n  private int copyInputToOutput(int positionFrames) {\n    int frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount);\n    copyToOutput(inputBuffer, positionFrames, frameCount);\n    remainingInputToCopyFrameCount -= frameCount;\n    return frameCount;\n  }\n\n  private void downSampleInput(short[] samples, int position, int skip) {\n    // If skip is greater than one, average skip samples together and write them to the down-sample\n    // buffer. If channelCount is greater than one, mix the channels together as we down sample.\n    int frameCount = maxRequiredFrameCount / skip;\n    int samplesPerValue = channelCount * skip;\n    position *= channelCount;\n    for (int i = 0; i < frameCount; i++) {\n      int value = 0;\n      for (int j = 0; j < samplesPerValue; j++) {\n        value += samples[position + i * samplesPerValue + j];\n      }\n      value /= samplesPerValue;\n      downSampleBuffer[i] = (short) value;\n    }\n  }\n\n  private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) {\n    // Find the best frequency match in the range, and given a sample skip multiple. For now, just\n    // find the pitch of the first channel.\n    int bestPeriod = 0;\n    int worstPeriod = 255;\n    int minDiff = 1;\n    int maxDiff = 0;\n    position *= channelCount;\n    for (int period = minPeriod; period <= maxPeriod; period++) {\n      int diff = 0;\n      for (int i = 0; i < period; i++) {\n        short sVal = samples[position + i];\n        short pVal = samples[position + period + i];\n        diff += Math.abs(sVal - pVal);\n      }\n      // Note that the highest number of samples we add into diff will be less than 256, since we\n      // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples\n      // without overflow.\n      if (diff * bestPeriod < minDiff * period) {\n        minDiff = diff;\n        bestPeriod = period;\n      }\n      if (diff * worstPeriod > maxDiff * period) {\n        maxDiff = diff;\n        worstPeriod = period;\n      }\n    }\n    this.minDiff = minDiff / bestPeriod;\n    this.maxDiff = maxDiff / worstPeriod;\n    return bestPeriod;\n  }\n\n  /**\n   * Returns whether the previous pitch period estimate is a better approximation, which can occur\n   * at the abrupt end of voiced words.\n   */\n  private boolean previousPeriodBetter(int minDiff, int maxDiff) {\n    if (minDiff == 0 || prevPeriod == 0) {\n      return false;\n    }\n    if (maxDiff > minDiff * 3) {\n      // Got a reasonable match this period.\n      return false;\n    }\n    if (minDiff * 2 <= prevMinDiff * 3) {\n      // Mismatch is not that much greater this period.\n      return false;\n    }\n    return true;\n  }\n\n  private int findPitchPeriod(short[] samples, int position) {\n    // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a\n    // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor\n    // get in the 11 kHz range, and then do it again with a narrower frequency range without down\n    // sampling.\n    int period;\n    int retPeriod;\n    int skip = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1;\n    if (channelCount == 1 && skip == 1) {\n      period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);\n    } else {\n      downSampleInput(samples, position, skip);\n      period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip);\n      if (skip != 1) {\n        period *= skip;\n        int minP = period - (skip * 4);\n        int maxP = period + (skip * 4);\n        if (minP < minPeriod) {\n          minP = minPeriod;\n        }\n        if (maxP > maxPeriod) {\n          maxP = maxPeriod;\n        }\n        if (channelCount == 1) {\n          period = findPitchPeriodInRange(samples, position, minP, maxP);\n        } else {\n          downSampleInput(samples, position, 1);\n          period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP);\n        }\n      }\n    }\n    if (previousPeriodBetter(minDiff, maxDiff)) {\n      retPeriod = prevPeriod;\n    } else {\n      retPeriod = period;\n    }\n    prevMinDiff = minDiff;\n    prevPeriod = period;\n    return retPeriod;\n  }\n\n  private void moveNewSamplesToPitchBuffer(int originalOutputFrameCount) {\n    int frameCount = outputFrameCount - originalOutputFrameCount;\n    pitchBuffer = ensureSpaceForAdditionalFrames(pitchBuffer, pitchFrameCount, frameCount);\n    System.arraycopy(\n        outputBuffer,\n        originalOutputFrameCount * channelCount,\n        pitchBuffer,\n        pitchFrameCount * channelCount,\n        frameCount * channelCount);\n    outputFrameCount = originalOutputFrameCount;\n    pitchFrameCount += frameCount;\n  }\n\n  private void removePitchFrames(int frameCount) {\n    if (frameCount == 0) {\n      return;\n    }\n    System.arraycopy(\n        pitchBuffer,\n        frameCount * channelCount,\n        pitchBuffer,\n        0,\n        (pitchFrameCount - frameCount) * channelCount);\n    pitchFrameCount -= frameCount;\n  }\n\n  private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {\n    short left = in[inPos];\n    short right = in[inPos + channelCount];\n    int position = newRatePosition * oldSampleRate;\n    int leftPosition = oldRatePosition * newSampleRate;\n    int rightPosition = (oldRatePosition + 1) * newSampleRate;\n    int ratio = rightPosition - position;\n    int width = rightPosition - leftPosition;\n    return (short) ((ratio * left + (width - ratio) * right) / width);\n  }\n\n  private void adjustRate(float rate, int originalOutputFrameCount) {\n    if (outputFrameCount == originalOutputFrameCount) {\n      return;\n    }\n    int newSampleRate = (int) (inputSampleRateHz / rate);\n    int oldSampleRate = inputSampleRateHz;\n    // Set these values to help with the integer math.\n    while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) {\n      newSampleRate /= 2;\n      oldSampleRate /= 2;\n    }\n    moveNewSamplesToPitchBuffer(originalOutputFrameCount);\n    // Leave at least one pitch sample in the buffer.\n    for (int position = 0; position < pitchFrameCount - 1; position++) {\n      while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {\n        outputBuffer =\n            ensureSpaceForAdditionalFrames(\n                outputBuffer, outputFrameCount, /* additionalFrameCount= */ 1);\n        for (int i = 0; i < channelCount; i++) {\n          outputBuffer[outputFrameCount * channelCount + i] =\n              interpolate(pitchBuffer, position * channelCount + i, oldSampleRate, newSampleRate);\n        }\n        newRatePosition++;\n        outputFrameCount++;\n      }\n      oldRatePosition++;\n      if (oldRatePosition == oldSampleRate) {\n        oldRatePosition = 0;\n        Assertions.checkState(newRatePosition == newSampleRate);\n        newRatePosition = 0;\n      }\n    }\n    removePitchFrames(pitchFrameCount - 1);\n  }\n\n  private int skipPitchPeriod(short[] samples, int position, float speed, int period) {\n    // Skip over a pitch period, and copy period/speed samples to the output.\n    int newFrameCount;\n    if (speed >= 2.0f) {\n      newFrameCount = (int) (period / (speed - 1.0f));\n    } else {\n      newFrameCount = period;\n      remainingInputToCopyFrameCount = (int) (period * (2.0f - speed) / (speed - 1.0f));\n    }\n    outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount);\n    overlapAdd(\n        newFrameCount,\n        channelCount,\n        outputBuffer,\n        outputFrameCount,\n        samples,\n        position,\n        samples,\n        position + period);\n    outputFrameCount += newFrameCount;\n    return newFrameCount;\n  }\n\n  private int insertPitchPeriod(short[] samples, int position, float speed, int period) {\n    // Insert a pitch period, and determine how much input to copy directly.\n    int newFrameCount;\n    if (speed < 0.5f) {\n      newFrameCount = (int) (period * speed / (1.0f - speed));\n    } else {\n      newFrameCount = period;\n      remainingInputToCopyFrameCount = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));\n    }\n    outputBuffer =\n        ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, period + newFrameCount);\n    System.arraycopy(\n        samples,\n        position * channelCount,\n        outputBuffer,\n        outputFrameCount * channelCount,\n        period * channelCount);\n    overlapAdd(\n        newFrameCount,\n        channelCount,\n        outputBuffer,\n        outputFrameCount + period,\n        samples,\n        position + period,\n        samples,\n        position);\n    outputFrameCount += period + newFrameCount;\n    return newFrameCount;\n  }\n\n  private void changeSpeed(float speed) {\n    if (inputFrameCount < maxRequiredFrameCount) {\n      return;\n    }\n    int frameCount = inputFrameCount;\n    int positionFrames = 0;\n    do {\n      if (remainingInputToCopyFrameCount > 0) {\n        positionFrames += copyInputToOutput(positionFrames);\n      } else {\n        int period = findPitchPeriod(inputBuffer, positionFrames);\n        if (speed > 1.0) {\n          positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period);\n        } else {\n          positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period);\n        }\n      }\n    } while (positionFrames + maxRequiredFrameCount <= frameCount);\n    removeProcessedInputFrames(positionFrames);\n  }\n\n  private void processStreamInput() {\n    // Resample as many pitch periods as we have buffered on the input.\n    int originalOutputFrameCount = outputFrameCount;\n    float s = speed / pitch;\n    float r = rate * pitch;\n    if (s > 1.00001 || s < 0.99999) {\n      changeSpeed(s);\n    } else {\n      copyToOutput(inputBuffer, 0, inputFrameCount);\n      inputFrameCount = 0;\n    }\n    if (r != 1.0f) {\n      adjustRate(r, originalOutputFrameCount);\n    }\n  }\n\n  private static void overlapAdd(\n      int frameCount,\n      int channelCount,\n      short[] out,\n      int outPosition,\n      short[] rampDown,\n      int rampDownPosition,\n      short[] rampUp,\n      int rampUpPosition) {\n    for (int i = 0; i < channelCount; i++) {\n      int o = outPosition * channelCount + i;\n      int u = rampUpPosition * channelCount + i;\n      int d = rampDownPosition * channelCount + i;\n      for (int t = 0; t < frameCount; t++) {\n        out[o] = (short) ((rampDown[d] * (frameCount - t) + rampUp[u] * t) / frameCount);\n        o += channelCount;\n        d += channelCount;\n        u += channelCount;\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.ShortBuffer;\n\n/**\n * An {@link AudioProcessor} that uses the Sonic library to modify audio speed/pitch/sample rate.\n */\npublic final class SonicAudioProcessor implements AudioProcessor {\n\n  /**\n   * The maximum allowed playback speed in {@link #setSpeed(float)}.\n   */\n  public static final float MAXIMUM_SPEED = 8.0f;\n  /**\n   * The minimum allowed playback speed in {@link #setSpeed(float)}.\n   */\n  public static final float MINIMUM_SPEED = 0.1f;\n  /**\n   * The maximum allowed pitch in {@link #setPitch(float)}.\n   */\n  public static final float MAXIMUM_PITCH = 8.0f;\n  /**\n   * The minimum allowed pitch in {@link #setPitch(float)}.\n   */\n  public static final float MINIMUM_PITCH = 0.1f;\n  /**\n   * Indicates that the output sample rate should be the same as the input.\n   */\n  public static final int SAMPLE_RATE_NO_CHANGE = -1;\n\n  /**\n   * The threshold below which the difference between two pitch/speed factors is negligible.\n   */\n  private static final float CLOSE_THRESHOLD = 0.01f;\n\n  /**\n   * The minimum number of output bytes at which the speedup is calculated using the input/output\n   * byte counts, rather than using the current playback parameters speed.\n   */\n  private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024;\n\n  private int pendingOutputSampleRate;\n  private float speed;\n  private float pitch;\n\n  private AudioFormat pendingInputAudioFormat;\n  private AudioFormat pendingOutputAudioFormat;\n  private AudioFormat inputAudioFormat;\n  private AudioFormat outputAudioFormat;\n\n  private boolean pendingSonicRecreation;\n  @Nullable private Sonic sonic;\n  private ByteBuffer buffer;\n  private ShortBuffer shortBuffer;\n  private ByteBuffer outputBuffer;\n  private long inputBytes;\n  private long outputBytes;\n  private boolean inputEnded;\n\n  /**\n   * Creates a new Sonic audio processor.\n   */\n  public SonicAudioProcessor() {\n    speed = 1f;\n    pitch = 1f;\n    pendingInputAudioFormat = AudioFormat.NOT_SET;\n    pendingOutputAudioFormat = AudioFormat.NOT_SET;\n    inputAudioFormat = AudioFormat.NOT_SET;\n    outputAudioFormat = AudioFormat.NOT_SET;\n    buffer = EMPTY_BUFFER;\n    shortBuffer = buffer.asShortBuffer();\n    outputBuffer = EMPTY_BUFFER;\n    pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;\n  }\n\n  /**\n   * Sets the playback speed. This method may only be called after draining data through the\n   * processor. The value returned by {@link #isActive()} may change, and the processor must be\n   * {@link #flush() flushed} before queueing more data.\n   *\n   * @param speed The requested new playback speed.\n   * @return The actual new playback speed.\n   */\n  public float setSpeed(float speed) {\n    speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);\n    if (this.speed != speed) {\n      this.speed = speed;\n      pendingSonicRecreation = true;\n    }\n    return speed;\n  }\n\n  /**\n   * Sets the playback pitch. This method may only be called after draining data through the\n   * processor. The value returned by {@link #isActive()} may change, and the processor must be\n   * {@link #flush() flushed} before queueing more data.\n   *\n   * @param pitch The requested new pitch.\n   * @return The actual new pitch.\n   */\n  public float setPitch(float pitch) {\n    pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);\n    if (this.pitch != pitch) {\n      this.pitch = pitch;\n      pendingSonicRecreation = true;\n    }\n    return pitch;\n  }\n\n  /**\n   * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output\n   * audio at the same sample rate as the input. After calling this method, call {@link\n   * #configure(AudioFormat)} to configure the processor with the new sample rate.\n   *\n   * @param sampleRateHz The sample rate for output audio, in Hertz.\n   * @see #configure(AudioFormat)\n   */\n  public void setOutputSampleRateHz(int sampleRateHz) {\n    pendingOutputSampleRate = sampleRateHz;\n  }\n\n  /**\n   * Returns the specified duration scaled to take into account the speedup factor of this instance,\n   * in the same units as {@code duration}.\n   *\n   * @param duration The duration to scale taking into account speedup.\n   * @return The specified duration scaled to take into account speedup, in the same units as\n   *     {@code duration}.\n   */\n  public long scaleDurationForSpeedup(long duration) {\n    if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) {\n      return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate\n          ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes)\n          : Util.scaleLargeTimestamp(\n              duration,\n              inputBytes * outputAudioFormat.sampleRate,\n              outputBytes * inputAudioFormat.sampleRate);\n    } else {\n      return (long) ((double) speed * duration);\n    }\n  }\n\n  @Override\n  public AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException {\n    if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n    int outputSampleRateHz =\n        pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE\n            ? inputAudioFormat.sampleRate\n            : pendingOutputSampleRate;\n    pendingInputAudioFormat = inputAudioFormat;\n    pendingOutputAudioFormat =\n        new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT);\n    pendingSonicRecreation = true;\n    return pendingOutputAudioFormat;\n  }\n\n  @Override\n  public boolean isActive() {\n    return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE\n        && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD\n            || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD\n            || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    Sonic sonic = Assertions.checkNotNull(this.sonic);\n    if (inputBuffer.hasRemaining()) {\n      ShortBuffer shortBuffer = inputBuffer.asShortBuffer();\n      int inputSize = inputBuffer.remaining();\n      inputBytes += inputSize;\n      sonic.queueInput(shortBuffer);\n      inputBuffer.position(inputBuffer.position() + inputSize);\n    }\n    int outputSize = sonic.getOutputSize();\n    if (outputSize > 0) {\n      if (buffer.capacity() < outputSize) {\n        buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());\n        shortBuffer = buffer.asShortBuffer();\n      } else {\n        buffer.clear();\n        shortBuffer.clear();\n      }\n      sonic.getOutput(shortBuffer);\n      outputBytes += outputSize;\n      buffer.limit(outputSize);\n      outputBuffer = buffer;\n    }\n  }\n\n  @Override\n  public void queueEndOfStream() {\n    if (sonic != null) {\n      sonic.queueEndOfStream();\n    }\n    inputEnded = true;\n  }\n\n  @Override\n  public ByteBuffer getOutput() {\n    ByteBuffer outputBuffer = this.outputBuffer;\n    this.outputBuffer = EMPTY_BUFFER;\n    return outputBuffer;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return inputEnded && (sonic == null || sonic.getOutputSize() == 0);\n  }\n\n  @Override\n  public void flush() {\n    if (isActive()) {\n      inputAudioFormat = pendingInputAudioFormat;\n      outputAudioFormat = pendingOutputAudioFormat;\n      if (pendingSonicRecreation) {\n        sonic =\n            new Sonic(\n                inputAudioFormat.sampleRate,\n                inputAudioFormat.channelCount,\n                speed,\n                pitch,\n                outputAudioFormat.sampleRate);\n      } else if (sonic != null) {\n        sonic.flush();\n      }\n    }\n    outputBuffer = EMPTY_BUFFER;\n    inputBytes = 0;\n    outputBytes = 0;\n    inputEnded = false;\n  }\n\n  @Override\n  public void reset() {\n    speed = 1f;\n    pitch = 1f;\n    pendingInputAudioFormat = AudioFormat.NOT_SET;\n    pendingOutputAudioFormat = AudioFormat.NOT_SET;\n    inputAudioFormat = AudioFormat.NOT_SET;\n    outputAudioFormat = AudioFormat.NOT_SET;\n    buffer = EMPTY_BUFFER;\n    shortBuffer = buffer.asShortBuffer();\n    outputBuffer = EMPTY_BUFFER;\n    pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;\n    pendingSonicRecreation = false;\n    sonic = null;\n    inputBytes = 0;\n    outputBytes = 0;\n    inputEnded = false;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * Audio processor that outputs its input unmodified and also outputs its input to a given sink.\n * This is intended to be used for diagnostics and debugging.\n *\n * <p>This audio processor can be inserted into the audio processor chain to access audio data\n * before/after particular processing steps have been applied. For example, to get audio output\n * after playback speed adjustment and silence skipping have been applied it is necessary to pass a\n * custom {@link DefaultAudioSink.AudioProcessorChain} when\n * creating the audio sink, and include this audio processor after all other audio processors.\n */\npublic final class TeeAudioProcessor extends BaseAudioProcessor {\n\n  /** A sink for audio buffers handled by the audio processor. */\n  public interface AudioBufferSink {\n\n    /** Called when the audio processor is flushed with a format of subsequent input. */\n    void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding);\n\n    /**\n     * Called when data is written to the audio processor.\n     *\n     * @param buffer A read-only buffer containing input which the audio processor will handle.\n     */\n    void handleBuffer(ByteBuffer buffer);\n  }\n\n  private final AudioBufferSink audioBufferSink;\n\n  /**\n   * Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}.\n   *\n   * @param audioBufferSink The audio buffer sink that will receive input queued to this audio\n   *     processor.\n   */\n  public TeeAudioProcessor(AudioBufferSink audioBufferSink) {\n    this.audioBufferSink = Assertions.checkNotNull(audioBufferSink);\n  }\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat) {\n    // This processor is always active (if passed to the sink) and outputs its input.\n    return inputAudioFormat;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    int remaining = inputBuffer.remaining();\n    if (remaining == 0) {\n      return;\n    }\n    audioBufferSink.handleBuffer(inputBuffer.asReadOnlyBuffer());\n    replaceOutputBuffer(remaining).put(inputBuffer).flip();\n  }\n\n  @Override\n  protected void onQueueEndOfStream() {\n    flushSinkIfActive();\n  }\n\n  @Override\n  protected void onReset() {\n    flushSinkIfActive();\n  }\n\n  private void flushSinkIfActive() {\n    if (isActive()) {\n      audioBufferSink.flush(\n          inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding);\n    }\n  }\n\n  /**\n   * A sink for audio buffers that writes output audio as .wav files with a given path prefix. When\n   * new audio data is handled after flushing the audio processor, a counter is incremented and its\n   * value is appended to the output file name.\n   *\n   * <p>Note: if writing to external storage it's necessary to grant the {@code\n   * WRITE_EXTERNAL_STORAGE} permission.\n   */\n  public static final class WavFileAudioBufferSink implements AudioBufferSink {\n\n    private static final String TAG = \"WaveFileAudioBufferSink\";\n\n    private static final int FILE_SIZE_MINUS_8_OFFSET = 4;\n    private static final int FILE_SIZE_MINUS_44_OFFSET = 40;\n    private static final int HEADER_LENGTH = 44;\n\n    private final String outputFileNamePrefix;\n    private final byte[] scratchBuffer;\n    private final ByteBuffer scratchByteBuffer;\n\n    private int sampleRateHz;\n    private int channelCount;\n    @C.PcmEncoding private int encoding;\n    @Nullable private RandomAccessFile randomAccessFile;\n    private int counter;\n    private int bytesWritten;\n\n    /**\n     * Creates a new audio buffer sink that writes to .wav files with the given prefix.\n     *\n     * @param outputFileNamePrefix The prefix for output files.\n     */\n    public WavFileAudioBufferSink(String outputFileNamePrefix) {\n      this.outputFileNamePrefix = outputFileNamePrefix;\n      scratchBuffer = new byte[1024];\n      scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN);\n    }\n\n    @Override\n    public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {\n      try {\n        reset();\n      } catch (IOException e) {\n        Log.e(TAG, \"Error resetting\", e);\n      }\n      this.sampleRateHz = sampleRateHz;\n      this.channelCount = channelCount;\n      this.encoding = encoding;\n    }\n\n    @Override\n    public void handleBuffer(ByteBuffer buffer) {\n      try {\n        maybePrepareFile();\n        writeBuffer(buffer);\n      } catch (IOException e) {\n        Log.e(TAG, \"Error writing data\", e);\n      }\n    }\n\n    private void maybePrepareFile() throws IOException {\n      if (randomAccessFile != null) {\n        return;\n      }\n      RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), \"rw\");\n      writeFileHeader(randomAccessFile);\n      this.randomAccessFile = randomAccessFile;\n      bytesWritten = HEADER_LENGTH;\n    }\n\n    private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException {\n      // Write the start of the header as big endian data.\n      randomAccessFile.writeInt(WavUtil.RIFF_FOURCC);\n      randomAccessFile.writeInt(-1);\n      randomAccessFile.writeInt(WavUtil.WAVE_FOURCC);\n      randomAccessFile.writeInt(WavUtil.FMT_FOURCC);\n\n      // Write the rest of the header as little endian data.\n      scratchByteBuffer.clear();\n      scratchByteBuffer.putInt(16);\n      scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding));\n      scratchByteBuffer.putShort((short) channelCount);\n      scratchByteBuffer.putInt(sampleRateHz);\n      int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);\n      scratchByteBuffer.putInt(bytesPerSample * sampleRateHz);\n      scratchByteBuffer.putShort((short) bytesPerSample);\n      scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount));\n      randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position());\n\n      // Write the start of the data chunk as big endian data.\n      randomAccessFile.writeInt(WavUtil.DATA_FOURCC);\n      randomAccessFile.writeInt(-1);\n    }\n\n    private void writeBuffer(ByteBuffer buffer) throws IOException {\n      RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile);\n      while (buffer.hasRemaining()) {\n        int bytesToWrite = Math.min(buffer.remaining(), scratchBuffer.length);\n        buffer.get(scratchBuffer, 0, bytesToWrite);\n        randomAccessFile.write(scratchBuffer, 0, bytesToWrite);\n        bytesWritten += bytesToWrite;\n      }\n    }\n\n    private void reset() throws IOException {\n      RandomAccessFile randomAccessFile = this.randomAccessFile;\n      if (randomAccessFile == null) {\n        return;\n      }\n\n      try {\n        scratchByteBuffer.clear();\n        scratchByteBuffer.putInt(bytesWritten - 8);\n        randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET);\n        randomAccessFile.write(scratchBuffer, 0, 4);\n\n        scratchByteBuffer.clear();\n        scratchByteBuffer.putInt(bytesWritten - 44);\n        randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET);\n        randomAccessFile.write(scratchBuffer, 0, 4);\n      } catch (IOException e) {\n        // The file may still be playable, so just log a warning.\n        Log.w(TAG, \"Error updating file size\", e);\n      }\n\n      try {\n        randomAccessFile.close();\n      } finally {\n        this.randomAccessFile = null;\n      }\n    }\n\n    private String getNextOutputFileName() {\n      return Util.formatInvariant(\"%s-%04d.wav\", outputFileNamePrefix, counter++);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\n\n/** Audio processor for trimming samples from the start/end of data. */\n/* package */ final class TrimmingAudioProcessor extends BaseAudioProcessor {\n\n  @C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT;\n\n  private int trimStartFrames;\n  private int trimEndFrames;\n  private boolean reconfigurationPending;\n\n  private int pendingTrimStartBytes;\n  private byte[] endBuffer;\n  private int endBufferSize;\n  private long trimmedFrameCount;\n\n  /** Creates a new audio processor for trimming samples from the start/end of data. */\n  public TrimmingAudioProcessor() {\n    endBuffer = Util.EMPTY_BYTE_ARRAY;\n  }\n\n  /**\n   * Sets the number of audio frames to trim from the start and end of audio passed to this\n   * processor. After calling this method, call {@link #configure(AudioFormat)} to apply the new\n   * trimming frame counts.\n   *\n   * @param trimStartFrames The number of audio frames to trim from the start of audio.\n   * @param trimEndFrames The number of audio frames to trim from the end of audio.\n   * @see AudioSink#configure(int, int, int, int, int[], int, int)\n   */\n  public void setTrimFrameCount(int trimStartFrames, int trimEndFrames) {\n    this.trimStartFrames = trimStartFrames;\n    this.trimEndFrames = trimEndFrames;\n  }\n\n  /** Sets the trimmed frame count returned by {@link #getTrimmedFrameCount()} to zero. */\n  public void resetTrimmedFrameCount() {\n    trimmedFrameCount = 0;\n  }\n\n  /**\n   * Returns the number of audio frames trimmed since the last call to {@link\n   * #resetTrimmedFrameCount()}.\n   */\n  public long getTrimmedFrameCount() {\n    return trimmedFrameCount;\n  }\n\n  @Override\n  public AudioFormat onConfigure(AudioFormat inputAudioFormat)\n      throws UnhandledAudioFormatException {\n    if (inputAudioFormat.encoding != OUTPUT_ENCODING) {\n      throw new UnhandledAudioFormatException(inputAudioFormat);\n    }\n    reconfigurationPending = true;\n    return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    int position = inputBuffer.position();\n    int limit = inputBuffer.limit();\n    int remaining = limit - position;\n\n    if (remaining == 0) {\n      return;\n    }\n\n    // Trim any pending start bytes from the input buffer.\n    int trimBytes = Math.min(remaining, pendingTrimStartBytes);\n    trimmedFrameCount += trimBytes / inputAudioFormat.bytesPerFrame;\n    pendingTrimStartBytes -= trimBytes;\n    inputBuffer.position(position + trimBytes);\n    if (pendingTrimStartBytes > 0) {\n      // Nothing to output yet.\n      return;\n    }\n    remaining -= trimBytes;\n\n    // endBuffer must be kept as full as possible, so that we trim the right amount of media if we\n    // don't receive any more input. After taking into account the number of bytes needed to keep\n    // endBuffer as full as possible, the output should be any surplus bytes currently in endBuffer\n    // followed by any surplus bytes in the new inputBuffer.\n    int remainingBytesToOutput = endBufferSize + remaining - endBuffer.length;\n    ByteBuffer buffer = replaceOutputBuffer(remainingBytesToOutput);\n\n    // Output from endBuffer.\n    int endBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, endBufferSize);\n    buffer.put(endBuffer, 0, endBufferBytesToOutput);\n    remainingBytesToOutput -= endBufferBytesToOutput;\n\n    // Output from inputBuffer, restoring its limit afterwards.\n    int inputBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, remaining);\n    inputBuffer.limit(inputBuffer.position() + inputBufferBytesToOutput);\n    buffer.put(inputBuffer);\n    inputBuffer.limit(limit);\n    remaining -= inputBufferBytesToOutput;\n\n    // Compact endBuffer, then repopulate it using the new input.\n    endBufferSize -= endBufferBytesToOutput;\n    System.arraycopy(endBuffer, endBufferBytesToOutput, endBuffer, 0, endBufferSize);\n    inputBuffer.get(endBuffer, endBufferSize, remaining);\n    endBufferSize += remaining;\n\n    buffer.flip();\n  }\n\n  @Override\n  public ByteBuffer getOutput() {\n    if (super.isEnded() && endBufferSize > 0) {\n      // Because audio processors may be drained in the middle of the stream we assume that the\n      // contents of the end buffer need to be output. For gapless transitions, configure will\n      // always be called, so the end buffer is cleared in onQueueEndOfStream.\n      replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip();\n      endBufferSize = 0;\n    }\n    return super.getOutput();\n  }\n\n  @Override\n  public boolean isEnded() {\n    return super.isEnded() && endBufferSize == 0;\n  }\n\n  @Override\n  protected void onQueueEndOfStream() {\n    if (reconfigurationPending) {\n      // Trim audio in the end buffer.\n      if (endBufferSize > 0) {\n        trimmedFrameCount += endBufferSize / inputAudioFormat.bytesPerFrame;\n      }\n      endBufferSize = 0;\n    }\n  }\n\n  @Override\n  protected void onFlush() {\n    if (reconfigurationPending) {\n      reconfigurationPending = false;\n      endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame];\n      pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame;\n    } else {\n      // Audio processors are flushed after initial configuration, so we leave the pending trim\n      // start byte count unmodified if the processor was just configured. Otherwise we (possibly\n      // incorrectly) assume that this is a seek to a non-zero position. We should instead check the\n      // timestamp of the first input buffer queued after flushing to decide whether to trim (see\n      // also [Internal: b/77292509]).\n      pendingTrimStartBytes = 0;\n    }\n    endBufferSize = 0;\n  }\n\n  @Override\n  protected void onReset() {\n    endBuffer = Util.EMPTY_BYTE_ARRAY;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Utilities for handling WAVE files. */\npublic final class WavUtil {\n\n  /** Four character code for \"RIFF\". */\n  public static final int RIFF_FOURCC = 0x52494646;\n  /** Four character code for \"WAVE\". */\n  public static final int WAVE_FOURCC = 0x57415645;\n  /** Four character code for \"fmt \". */\n  public static final int FMT_FOURCC = 0x666d7420;\n  /** Four character code for \"data\". */\n  public static final int DATA_FOURCC = 0x64617461;\n\n  /** WAVE type value for integer PCM audio data. */\n  private static final int TYPE_PCM = 0x0001;\n  /** WAVE type value for float PCM audio data. */\n  private static final int TYPE_FLOAT = 0x0003;\n  /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */\n  private static final int TYPE_A_LAW = 0x0006;\n  /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */\n  private static final int TYPE_MU_LAW = 0x0007;\n  /** WAVE type value for extended WAVE format. */\n  private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;\n\n  /** Returns the WAVE type value for the given {@code encoding}. */\n  public static int getTypeForEncoding(@C.PcmEncoding int encoding) {\n    switch (encoding) {\n      case C.ENCODING_PCM_8BIT:\n      case C.ENCODING_PCM_16BIT:\n      case C.ENCODING_PCM_24BIT:\n      case C.ENCODING_PCM_32BIT:\n        return TYPE_PCM;\n      case C.ENCODING_PCM_A_LAW:\n        return TYPE_A_LAW;\n      case C.ENCODING_PCM_MU_LAW:\n        return TYPE_MU_LAW;\n      case C.ENCODING_PCM_FLOAT:\n        return TYPE_FLOAT;\n      case C.ENCODING_INVALID:\n      case Format.NO_VALUE:\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  /** Returns the PCM encoding for the given WAVE {@code type} value. */\n  public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) {\n    switch (type) {\n      case TYPE_PCM:\n      case TYPE_WAVE_FORMAT_EXTENSIBLE:\n        return Util.getPcmEncoding(bitsPerSample);\n      case TYPE_FLOAT:\n        return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;\n      case TYPE_A_LAW:\n        return C.ENCODING_PCM_A_LAW;\n      case TYPE_MU_LAW:\n        return C.ENCODING_PCM_MU_LAW;\n      default:\n        return C.ENCODING_INVALID;\n    }\n  }\n\n  private WavUtil() {\n    // Prevent instantiation.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/audio/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.audio;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/DatabaseIOException.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.database;\n\nimport android.database.SQLException;\nimport java.io.IOException;\n\n/** An {@link IOException} whose cause is an {@link SQLException}. */\npublic final class DatabaseIOException extends IOException {\n\n  public DatabaseIOException(SQLException cause) {\n    super(cause);\n  }\n\n  public DatabaseIOException(SQLException cause, String message) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.database;\n\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\n\n/**\n * Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write\n * tables prefixed with {@link #TABLE_PREFIX}.\n */\npublic interface DatabaseProvider {\n\n  /** Prefix for tables that can be read and written by ExoPlayer components. */\n  String TABLE_PREFIX = \"ExoPlayer\";\n\n  /**\n   * Creates and/or opens a database that will be used for reading and writing.\n   *\n   * <p>Once opened successfully, the database is cached, so you can call this method every time you\n   * need to write to the database. Errors such as bad permissions or a full disk may cause this\n   * method to fail, but future attempts may succeed if the problem is fixed.\n   *\n   * @throws SQLiteException If the database cannot be opened for writing.\n   * @return A read/write database object.\n   */\n  SQLiteDatabase getWritableDatabase();\n\n  /**\n   * Creates and/or opens a database. This will be the same object returned by {@link\n   * #getWritableDatabase()} unless some problem, such as a full disk, requires the database to be\n   * opened read-only. In that case, a read-only database object will be returned. If the problem is\n   * fixed, a future call to {@link #getWritableDatabase()} may succeed, in which case the read-only\n   * database object will be closed and the read/write object will be returned in the future.\n   *\n   * <p>Once opened successfully, the database is cached, so you can call this method every time you\n   * need to read from the database.\n   *\n   * @throws SQLiteException If the database cannot be opened.\n   * @return A database object valid until {@link #getWritableDatabase()} is called.\n   */\n  SQLiteDatabase getReadableDatabase();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.database;\n\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\n\n/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */\npublic final class DefaultDatabaseProvider implements DatabaseProvider {\n\n  private final SQLiteOpenHelper sqliteOpenHelper;\n\n  /**\n   * @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances.\n   */\n  public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) {\n    this.sqliteOpenHelper = sqliteOpenHelper;\n  }\n\n  @Override\n  public SQLiteDatabase getWritableDatabase() {\n    return sqliteOpenHelper.getWritableDatabase();\n  }\n\n  @Override\n  public SQLiteDatabase getReadableDatabase() {\n    return sqliteOpenHelper.getReadableDatabase();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.database;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database.\n *\n * <p>Suitable for use by applications that do not already have their own database, or that would\n * prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer\n * to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}.\n */\npublic final class ExoDatabaseProvider extends SQLiteOpenHelper implements DatabaseProvider {\n\n  /** The file name used for the standalone ExoPlayer database. */\n  public static final String DATABASE_NAME = \"exoplayer_internal.db\";\n\n  private static final int VERSION = 1;\n  private static final String TAG = \"ExoDatabaseProvider\";\n\n  /**\n   * Provides instances of the database located by passing {@link #DATABASE_NAME} to {@link\n   * Context#getDatabasePath(String)}.\n   *\n   * @param context Any context.\n   */\n  public ExoDatabaseProvider(Context context) {\n    super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION);\n  }\n\n  @Override\n  public void onCreate(SQLiteDatabase db) {\n    // Features create their own tables.\n  }\n\n  @Override\n  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n    // Features handle their own upgrades.\n  }\n\n  @Override\n  public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n    wipeDatabase(db);\n  }\n\n  /**\n   * Makes a best effort to wipe the existing database. The wipe may be incomplete if the database\n   * contains foreign key constraints.\n   */\n  private static void wipeDatabase(SQLiteDatabase db) {\n    String[] columns = {\"type\", \"name\"};\n    try (Cursor cursor =\n        db.query(\n            \"sqlite_master\",\n            columns,\n            /* selection= */ null,\n            /* selectionArgs= */ null,\n            /* groupBy= */ null,\n            /* having= */ null,\n            /* orderBy= */ null)) {\n      while (cursor.moveToNext()) {\n        String type = cursor.getString(0);\n        String name = cursor.getString(1);\n        if (!\"sqlite_sequence\".equals(name)) {\n          // If it's not an SQL-controlled entity, drop it\n          String sql = \"DROP \" + type + \" IF EXISTS \" + name;\n          try {\n            db.execSQL(sql);\n          } catch (SQLException e) {\n            Log.e(TAG, \"Error executing \" + sql, e);\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/VersionTable.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.database;\n\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.DatabaseUtils;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.VisibleForTesting;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Utility methods for accessing versions of ExoPlayer database components. This allows them to be\n * versioned independently to the version of the containing database.\n */\npublic final class VersionTable {\n\n  /** Returned by {@link #getVersion(SQLiteDatabase, int, String)} if the version is unset. */\n  public static final int VERSION_UNSET = -1;\n  /** Version of tables used for offline functionality. */\n  public static final int FEATURE_OFFLINE = 0;\n  /** Version of tables used for cache content metadata. */\n  public static final int FEATURE_CACHE_CONTENT_METADATA = 1;\n  /** Version of tables used for cache file metadata. */\n  public static final int FEATURE_CACHE_FILE_METADATA = 2;\n\n  private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + \"Versions\";\n\n  private static final String COLUMN_FEATURE = \"feature\";\n  private static final String COLUMN_INSTANCE_UID = \"instance_uid\";\n  private static final String COLUMN_VERSION = \"version\";\n\n  private static final String WHERE_FEATURE_AND_INSTANCE_UID_EQUALS =\n      COLUMN_FEATURE + \" = ? AND \" + COLUMN_INSTANCE_UID + \" = ?\";\n\n  private static final String PRIMARY_KEY =\n      \"PRIMARY KEY (\" + COLUMN_FEATURE + \", \" + COLUMN_INSTANCE_UID + \")\";\n  private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS =\n      \"CREATE TABLE IF NOT EXISTS \"\n          + TABLE_NAME\n          + \" (\"\n          + COLUMN_FEATURE\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_INSTANCE_UID\n          + \" TEXT NOT NULL,\"\n          + COLUMN_VERSION\n          + \" INTEGER NOT NULL,\"\n          + PRIMARY_KEY\n          + \")\";\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA})\n  private @interface Feature {}\n\n  private VersionTable() {}\n\n  /**\n   * Sets the version of a specified instance of a specified feature.\n   *\n   * @param writableDatabase The database to update.\n   * @param feature The feature.\n   * @param instanceUid The unique identifier of the instance of the feature.\n   * @param version The version.\n   * @throws DatabaseIOException If an error occurs executing the SQL.\n   */\n  public static void setVersion(\n      SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version)\n      throws DatabaseIOException {\n    try {\n      writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_FEATURE, feature);\n      values.put(COLUMN_INSTANCE_UID, instanceUid);\n      values.put(COLUMN_VERSION, version);\n      writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values);\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Removes the version of a specified instance of a feature.\n   *\n   * @param writableDatabase The database to update.\n   * @param feature The feature.\n   * @param instanceUid The unique identifier of the instance of the feature.\n   * @throws DatabaseIOException If an error occurs executing the SQL.\n   */\n  public static void removeVersion(\n      SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid)\n      throws DatabaseIOException {\n    try {\n      if (!tableExists(writableDatabase, TABLE_NAME)) {\n        return;\n      }\n      writableDatabase.delete(\n          TABLE_NAME,\n          WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,\n          featureAndInstanceUidArguments(feature, instanceUid));\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Returns the version of a specified instance of a feature, or {@link #VERSION_UNSET} if no\n   * version is set.\n   *\n   * @param database The database to query.\n   * @param feature The feature.\n   * @param instanceUid The unique identifier of the instance of the feature.\n   * @return The version, or {@link #VERSION_UNSET} if no version is set.\n   * @throws DatabaseIOException If an error occurs executing the SQL.\n   */\n  public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid)\n      throws DatabaseIOException {\n    try {\n      if (!tableExists(database, TABLE_NAME)) {\n        return VERSION_UNSET;\n      }\n      try (Cursor cursor =\n          database.query(\n              TABLE_NAME,\n              new String[] {COLUMN_VERSION},\n              WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,\n              featureAndInstanceUidArguments(feature, instanceUid),\n              /* groupBy= */ null,\n              /* having= */ null,\n              /* orderBy= */ null)) {\n        if (cursor.getCount() == 0) {\n          return VERSION_UNSET;\n        }\n        cursor.moveToNext();\n        return cursor.getInt(/* COLUMN_VERSION index */ 0);\n      }\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @VisibleForTesting\n  /* package */ static boolean tableExists(SQLiteDatabase readableDatabase, String tableName) {\n    long count =\n        DatabaseUtils.queryNumEntries(\n            readableDatabase, \"sqlite_master\", \"tbl_name = ?\", new String[] {tableName});\n    return count > 0;\n  }\n\n  private static String[] featureAndInstanceUidArguments(int feature, String instance) {\n    return new String[] {Integer.toString(feature), instance};\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/database/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.database;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport com.google.android.exoplayer2.C;\n\n/**\n * Base class for buffers with flags.\n */\npublic abstract class Buffer {\n\n  @C.BufferFlags\n  private int flags;\n\n  /**\n   * Clears the buffer.\n   */\n  public void clear() {\n    flags = 0;\n  }\n\n  /**\n   * Returns whether the {@link C#BUFFER_FLAG_DECODE_ONLY} flag is set.\n   */\n  public final boolean isDecodeOnly() {\n    return getFlag(C.BUFFER_FLAG_DECODE_ONLY);\n  }\n\n  /**\n   * Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set.\n   */\n  public final boolean isEndOfStream() {\n    return getFlag(C.BUFFER_FLAG_END_OF_STREAM);\n  }\n\n  /**\n   * Returns whether the {@link C#BUFFER_FLAG_KEY_FRAME} flag is set.\n   */\n  public final boolean isKeyFrame() {\n    return getFlag(C.BUFFER_FLAG_KEY_FRAME);\n  }\n\n  /** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */\n  public final boolean hasSupplementalData() {\n    return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);\n  }\n\n  /**\n   * Replaces this buffer's flags with {@code flags}.\n   *\n   * @param flags The flags to set, which should be a combination of the {@code C.BUFFER_FLAG_*}\n   *     constants.\n   */\n  public final void setFlags(@C.BufferFlags int flags) {\n    this.flags = flags;\n  }\n\n  /**\n   * Adds the {@code flag} to this buffer's flags.\n   *\n   * @param flag The flag to add to this buffer's flags, which should be one of the\n   *     {@code C.BUFFER_FLAG_*} constants.\n   */\n  public final void addFlag(@C.BufferFlags int flag) {\n    flags |= flag;\n  }\n\n  /**\n   * Removes the {@code flag} from this buffer's flags, if it is set.\n   *\n   * @param flag The flag to remove.\n   */\n  public final void clearFlag(@C.BufferFlags int flag) {\n    flags &= ~flag;\n  }\n\n  /**\n   * Returns whether the specified flag has been set on this buffer.\n   *\n   * @param flag The flag to check.\n   * @return Whether the flag is set.\n   */\n  protected final boolean getFlag(@C.BufferFlags int flag) {\n    return (flags & flag) == flag;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport android.annotation.TargetApi;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}.\n */\npublic final class CryptoInfo {\n\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#iv\n   */\n  public byte[] iv;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#key\n   */\n  public byte[] key;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#mode\n   */\n  @C.CryptoMode\n  public int mode;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData\n   */\n  public int[] numBytesOfClearData;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData\n   */\n  public int[] numBytesOfEncryptedData;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#numSubSamples\n   */\n  public int numSubSamples;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo.Pattern\n   */\n  public int encryptedBlocks;\n  /**\n   * @see android.media.MediaCodec.CryptoInfo.Pattern\n   */\n  public int clearBlocks;\n\n  private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;\n  private final PatternHolderV24 patternHolder;\n\n  public CryptoInfo() {\n    frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo();\n    patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;\n  }\n\n  /**\n   * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)\n   */\n  public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,\n      byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) {\n    this.numSubSamples = numSubSamples;\n    this.numBytesOfClearData = numBytesOfClearData;\n    this.numBytesOfEncryptedData = numBytesOfEncryptedData;\n    this.key = key;\n    this.iv = iv;\n    this.mode = mode;\n    this.encryptedBlocks = encryptedBlocks;\n    this.clearBlocks = clearBlocks;\n    // Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary\n    // object allocation on Android N.\n    frameworkCryptoInfo.numSubSamples = numSubSamples;\n    frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;\n    frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData;\n    frameworkCryptoInfo.key = key;\n    frameworkCryptoInfo.iv = iv;\n    frameworkCryptoInfo.mode = mode;\n    if (Util.SDK_INT >= 24) {\n      patternHolder.set(encryptedBlocks, clearBlocks);\n    }\n  }\n\n  /**\n   * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.\n   *\n   * <p>Successive calls to this method on a single {@link CryptoInfo} will return the same\n   * instance. Changes to the {@link CryptoInfo} will be reflected in the returned object. The\n   * return object should not be modified directly.\n   *\n   * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.\n   */\n  public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfo() {\n    return frameworkCryptoInfo;\n  }\n\n  /** @deprecated Use {@link #getFrameworkCryptoInfo()}. */\n  @Deprecated\n  public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {\n    return getFrameworkCryptoInfo();\n  }\n\n  @TargetApi(24)\n  private static final class PatternHolderV24 {\n\n    private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;\n    private final android.media.MediaCodec.CryptoInfo.Pattern pattern;\n\n    private PatternHolderV24(android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) {\n      this.frameworkCryptoInfo = frameworkCryptoInfo;\n      pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0);\n    }\n\n    private void set(int encryptedBlocks, int clearBlocks) {\n      pattern.set(encryptedBlocks, clearBlocks);\n      frameworkCryptoInfo.setPattern(pattern);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport androidx.annotation.Nullable;\n\n/**\n * A media decoder.\n *\n * @param <I> The type of buffer input to the decoder.\n * @param <O> The type of buffer output from the decoder.\n * @param <E> The type of exception thrown from the decoder.\n */\npublic interface Decoder<I, O, E extends Exception> {\n\n  /**\n   * Returns the name of the decoder.\n   *\n   * @return The name of the decoder.\n   */\n  String getName();\n\n  /**\n   * Dequeues the next input buffer to be filled and queued to the decoder.\n   *\n   * @return The input buffer, which will have been cleared, or null if a buffer isn't available.\n   * @throws E If a decoder error has occurred.\n   */\n  @Nullable\n  I dequeueInputBuffer() throws E;\n\n  /**\n   * Queues an input buffer to the decoder.\n   *\n   * @param inputBuffer The input buffer.\n   * @throws E If a decoder error has occurred.\n   */\n  void queueInputBuffer(I inputBuffer) throws E;\n\n  /**\n   * Dequeues the next output buffer from the decoder.\n   *\n   * @return The output buffer, or null if an output buffer isn't available.\n   * @throws E If a decoder error has occurred.\n   */\n  @Nullable\n  O dequeueOutputBuffer() throws E;\n\n  /**\n   * Flushes the decoder. Ownership of dequeued input buffers is returned to the decoder. The caller\n   * is still responsible for releasing any dequeued output buffers.\n   */\n  void flush();\n\n  /**\n   * Releases the decoder. Must be called when the decoder is no longer needed.\n   */\n  void release();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\n/**\n * Maintains decoder event counts, for debugging purposes only.\n * <p>\n * Counters should be written from the playback thread only. Counters may be read from any thread.\n * To ensure that the counter values are made visible across threads, users of this class should\n * invoke {@link #ensureUpdated()} prior to reading and after writing.\n */\npublic final class DecoderCounters {\n\n  /**\n   * The number of times a decoder has been initialized.\n   */\n  public int decoderInitCount;\n  /**\n   * The number of times a decoder has been released.\n   */\n  public int decoderReleaseCount;\n  /**\n   * The number of queued input buffers.\n   */\n  public int inputBufferCount;\n  /**\n   * The number of skipped input buffers.\n   * <p>\n   * A skipped input buffer is an input buffer that was deliberately not sent to the decoder.\n   */\n  public int skippedInputBufferCount;\n  /**\n   * The number of rendered output buffers.\n   */\n  public int renderedOutputBufferCount;\n  /**\n   * The number of skipped output buffers.\n   * <p>\n   * A skipped output buffer is an output buffer that was deliberately not rendered.\n   */\n  public int skippedOutputBufferCount;\n  /**\n   * The number of dropped buffers.\n   * <p>\n   * A dropped buffer is an buffer that was supposed to be decoded/rendered, but was instead\n   * dropped because it could not be rendered in time.\n   */\n  public int droppedBufferCount;\n  /**\n   * The maximum number of dropped buffers without an interleaving rendered output buffer.\n   * <p>\n   * Skipped output buffers are ignored for the purposes of calculating this value.\n   */\n  public int maxConsecutiveDroppedBufferCount;\n  /**\n   * The number of times all buffers to a keyframe were dropped.\n   * <p>\n   * Each time buffers to a keyframe are dropped, this counter is increased by one, and the dropped\n   * buffer counters are increased by one (for the current output buffer) plus the number of buffers\n   * dropped from the source to advance to the keyframe.\n   */\n  public int droppedToKeyframeCount;\n\n  /**\n   * Should be called to ensure counter values are made visible across threads. The playback thread\n   * should call this method after updating the counter values. Any other thread should call this\n   * method before reading the counters.\n   */\n  public synchronized void ensureUpdated() {\n    // Do nothing. The use of synchronized ensures a memory barrier should another thread also\n    // call this method.\n  }\n\n  /**\n   * Merges the counts from {@code other} into this instance.\n   *\n   * @param other The {@link DecoderCounters} to merge into this instance.\n   */\n  public void merge(DecoderCounters other) {\n    decoderInitCount += other.decoderInitCount;\n    decoderReleaseCount += other.decoderReleaseCount;\n    inputBufferCount += other.inputBufferCount;\n    skippedInputBufferCount += other.skippedInputBufferCount;\n    renderedOutputBufferCount += other.renderedOutputBufferCount;\n    skippedOutputBufferCount += other.skippedOutputBufferCount;\n    droppedBufferCount += other.droppedBufferCount;\n    maxConsecutiveDroppedBufferCount = Math.max(maxConsecutiveDroppedBufferCount,\n        other.maxConsecutiveDroppedBufferCount);\n    droppedToKeyframeCount += other.droppedToKeyframeCount;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\n\n/**\n * Holds input for a decoder.\n */\npublic class DecoderInputBuffer extends Buffer {\n\n  /**\n   * The buffer replacement mode, which may disable replacement. One of {@link\n   * #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} or {@link\n   * #BUFFER_REPLACEMENT_MODE_DIRECT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    BUFFER_REPLACEMENT_MODE_DISABLED,\n    BUFFER_REPLACEMENT_MODE_NORMAL,\n    BUFFER_REPLACEMENT_MODE_DIRECT\n  })\n  public @interface BufferReplacementMode {}\n  /**\n   * Disallows buffer replacement.\n   */\n  public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;\n  /**\n   * Allows buffer replacement using {@link ByteBuffer#allocate(int)}.\n   */\n  public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;\n  /**\n   * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.\n   */\n  public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;\n\n  /**\n   * {@link CryptoInfo} for encrypted data.\n   */\n  public final CryptoInfo cryptoInfo;\n\n  /** The buffer's data, or {@code null} if no data has been set. */\n  @Nullable public ByteBuffer data;\n\n  /**\n   * The time at which the sample should be presented.\n   */\n  public long timeUs;\n\n  /**\n   * Supplemental data related to the buffer, if {@link #hasSupplementalData()} returns true. If\n   * present, the buffer is populated with supplemental data from position 0 to its limit.\n   */\n  @Nullable public ByteBuffer supplementalData;\n\n  @BufferReplacementMode private final int bufferReplacementMode;\n\n  /**\n   * Creates a new instance for which {@link #isFlagsOnly()} will return true.\n   *\n   * @return A new flags only input buffer.\n   */\n  public static DecoderInputBuffer newFlagsOnlyInstance() {\n    return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);\n  }\n\n  /**\n   * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One\n   *     of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and\n   *     {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.\n   */\n  public DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) {\n    this.cryptoInfo = new CryptoInfo();\n    this.bufferReplacementMode = bufferReplacementMode;\n  }\n\n  /**\n   * Clears {@link #supplementalData} and ensures that it's large enough to accommodate {@code\n   * length} bytes.\n   *\n   * @param length The length of the supplemental data that must be accommodated, in bytes.\n   */\n  @EnsuresNonNull(\"supplementalData\")\n  public void resetSupplementalData(int length) {\n    if (supplementalData == null || supplementalData.capacity() < length) {\n      supplementalData = ByteBuffer.allocate(length);\n    } else {\n      supplementalData.clear();\n    }\n  }\n\n  /**\n   * Ensures that {@link #data} is large enough to accommodate a write of a given length at its\n   * current position.\n   *\n   * <p>If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is\n   * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer}\n   * whose capacity is sufficient. Data up to the current position is copied to the new buffer.\n   *\n   * @param length The length of the write that must be accommodated, in bytes.\n   * @throws IllegalStateException If there is insufficient capacity to accommodate the write and\n   *     the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.\n   */\n  @EnsuresNonNull(\"data\")\n  public void ensureSpaceForWrite(int length) {\n    if (data == null) {\n      data = createReplacementByteBuffer(length);\n      return;\n    }\n    // Check whether the current buffer is sufficient.\n    int capacity = data.capacity();\n    int position = data.position();\n    int requiredCapacity = position + length;\n    if (capacity >= requiredCapacity) {\n      return;\n    }\n    // Instantiate a new buffer if possible.\n    ByteBuffer newData = createReplacementByteBuffer(requiredCapacity);\n    // Copy data up to the current position from the old buffer to the new one.\n    if (position > 0) {\n      data.flip();\n      newData.put(data);\n    }\n    // Set the new buffer.\n    data = newData;\n  }\n\n  /**\n   * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and\n   * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.\n   */\n  public final boolean isFlagsOnly() {\n    return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED;\n  }\n\n  /**\n   * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set.\n   */\n  public final boolean isEncrypted() {\n    return getFlag(C.BUFFER_FLAG_ENCRYPTED);\n  }\n\n  /**\n   * Flips {@link #data} and {@link #supplementalData} in preparation for being queued to a decoder.\n   *\n   * @see java.nio.Buffer#flip()\n   */\n  public final void flip() {\n    data.flip();\n    if (supplementalData != null) {\n      supplementalData.flip();\n    }\n  }\n\n  @Override\n  public void clear() {\n    super.clear();\n    if (data != null) {\n      data.clear();\n    }\n    if (supplementalData != null) {\n      supplementalData.clear();\n    }\n  }\n\n  private ByteBuffer createReplacementByteBuffer(int requiredCapacity) {\n    if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) {\n      return ByteBuffer.allocate(requiredCapacity);\n    } else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) {\n      return ByteBuffer.allocateDirect(requiredCapacity);\n    } else {\n      int currentCapacity = data == null ? 0 : data.capacity();\n      throw new IllegalStateException(\"Buffer too small (\" + currentCapacity + \" < \"\n          + requiredCapacity + \")\");\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\n/**\n * Output buffer decoded by a {@link Decoder}.\n */\npublic abstract class OutputBuffer extends Buffer {\n\n  /**\n   * The presentation timestamp for the buffer, in microseconds.\n   */\n  public long timeUs;\n\n  /**\n   * The number of buffers immediately prior to this one that were skipped in the {@link Decoder}.\n   */\n  public int skippedOutputBufferCount;\n\n  /**\n   * Releases the output buffer for reuse. Must be called when the buffer is no longer needed.\n   */\n  public abstract void release();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayDeque;\n\n/** Base class for {@link Decoder}s that use their own decode thread. */\n@SuppressWarnings(\"UngroupedOverloads\")\npublic abstract class SimpleDecoder<\n        I extends DecoderInputBuffer, O extends OutputBuffer, E extends Exception>\n    implements Decoder<I, O, E> {\n\n  private final Thread decodeThread;\n\n  private final Object lock;\n  private final ArrayDeque<I> queuedInputBuffers;\n  private final ArrayDeque<O> queuedOutputBuffers;\n  private final I[] availableInputBuffers;\n  private final O[] availableOutputBuffers;\n\n  private int availableInputBufferCount;\n  private int availableOutputBufferCount;\n  private I dequeuedInputBuffer;\n\n  private E exception;\n  private boolean flushed;\n  private boolean released;\n  private int skippedOutputBufferCount;\n\n  /**\n   * @param inputBuffers An array of nulls that will be used to store references to input buffers.\n   * @param outputBuffers An array of nulls that will be used to store references to output buffers.\n   */\n  protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {\n    lock = new Object();\n    queuedInputBuffers = new ArrayDeque<>();\n    queuedOutputBuffers = new ArrayDeque<>();\n    availableInputBuffers = inputBuffers;\n    availableInputBufferCount = inputBuffers.length;\n    for (int i = 0; i < availableInputBufferCount; i++) {\n      availableInputBuffers[i] = createInputBuffer();\n    }\n    availableOutputBuffers = outputBuffers;\n    availableOutputBufferCount = outputBuffers.length;\n    for (int i = 0; i < availableOutputBufferCount; i++) {\n      availableOutputBuffers[i] = createOutputBuffer();\n    }\n    decodeThread = new Thread() {\n      @Override\n      public void run() {\n        SimpleDecoder.this.run();\n      }\n    };\n    decodeThread.start();\n  }\n\n  /**\n   * Sets the initial size of each input buffer.\n   * <p>\n   * This method should only be called before the decoder is used (i.e. before the first call to\n   * {@link #dequeueInputBuffer()}.\n   *\n   * @param size The required input buffer size.\n   */\n  protected final void setInitialInputBufferSize(int size) {\n    Assertions.checkState(availableInputBufferCount == availableInputBuffers.length);\n    for (I inputBuffer : availableInputBuffers) {\n      inputBuffer.ensureSpaceForWrite(size);\n    }\n  }\n\n  @Override\n  @Nullable\n  public final I dequeueInputBuffer() throws E {\n    synchronized (lock) {\n      maybeThrowException();\n      Assertions.checkState(dequeuedInputBuffer == null);\n      dequeuedInputBuffer = availableInputBufferCount == 0 ? null\n          : availableInputBuffers[--availableInputBufferCount];\n      return dequeuedInputBuffer;\n    }\n  }\n\n  @Override\n  public final void queueInputBuffer(I inputBuffer) throws E {\n    synchronized (lock) {\n      maybeThrowException();\n      Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);\n      queuedInputBuffers.addLast(inputBuffer);\n      maybeNotifyDecodeLoop();\n      dequeuedInputBuffer = null;\n    }\n  }\n\n  @Override\n  @Nullable\n  public final O dequeueOutputBuffer() throws E {\n    synchronized (lock) {\n      maybeThrowException();\n      if (queuedOutputBuffers.isEmpty()) {\n        return null;\n      }\n      return queuedOutputBuffers.removeFirst();\n    }\n  }\n\n  /**\n   * Releases an output buffer back to the decoder.\n   *\n   * @param outputBuffer The output buffer being released.\n   */\n  @CallSuper\n  protected void releaseOutputBuffer(O outputBuffer) {\n    synchronized (lock) {\n      releaseOutputBufferInternal(outputBuffer);\n      maybeNotifyDecodeLoop();\n    }\n  }\n\n  @Override\n  public final void flush() {\n    synchronized (lock) {\n      flushed = true;\n      skippedOutputBufferCount = 0;\n      if (dequeuedInputBuffer != null) {\n        releaseInputBufferInternal(dequeuedInputBuffer);\n        dequeuedInputBuffer = null;\n      }\n      while (!queuedInputBuffers.isEmpty()) {\n        releaseInputBufferInternal(queuedInputBuffers.removeFirst());\n      }\n      while (!queuedOutputBuffers.isEmpty()) {\n        queuedOutputBuffers.removeFirst().release();\n      }\n    }\n  }\n\n  @CallSuper\n  @Override\n  public void release() {\n    synchronized (lock) {\n      released = true;\n      lock.notify();\n    }\n    try {\n      decodeThread.join();\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n    }\n  }\n\n  /**\n   * Throws a decode exception, if there is one.\n   *\n   * @throws E The decode exception.\n   */\n  private void maybeThrowException() throws E {\n    if (exception != null) {\n      throw exception;\n    }\n  }\n\n  /**\n   * Notifies the decode loop if there exists a queued input buffer and an available output buffer\n   * to decode into.\n   * <p>\n   * Should only be called whilst synchronized on the lock object.\n   */\n  private void maybeNotifyDecodeLoop() {\n    if (canDecodeBuffer()) {\n      lock.notify();\n    }\n  }\n\n  private void run() {\n    try {\n      while (decode()) {\n        // Do nothing.\n      }\n    } catch (InterruptedException e) {\n      // Not expected.\n      throw new IllegalStateException(e);\n    }\n  }\n\n  private boolean decode() throws InterruptedException {\n    I inputBuffer;\n    O outputBuffer;\n    boolean resetDecoder;\n\n    // Wait until we have an input buffer to decode, and an output buffer to decode into.\n    synchronized (lock) {\n      while (!released && !canDecodeBuffer()) {\n        lock.wait();\n      }\n      if (released) {\n        return false;\n      }\n      inputBuffer = queuedInputBuffers.removeFirst();\n      outputBuffer = availableOutputBuffers[--availableOutputBufferCount];\n      resetDecoder = flushed;\n      flushed = false;\n    }\n\n    if (inputBuffer.isEndOfStream()) {\n      outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n    } else {\n      if (inputBuffer.isDecodeOnly()) {\n        outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);\n      }\n      try {\n        exception = decode(inputBuffer, outputBuffer, resetDecoder);\n      } catch (RuntimeException e) {\n        // This can occur if a sample is malformed in a way that the decoder is not robust against.\n        // We don't want the process to die in this case, but we do want to propagate the error.\n        exception = createUnexpectedDecodeException(e);\n      } catch (OutOfMemoryError e) {\n        // This can occur if a sample is malformed in a way that causes the decoder to think it\n        // needs to allocate a large amount of memory. We don't want the process to die in this\n        // case, but we do want to propagate the error.\n        exception = createUnexpectedDecodeException(e);\n      }\n      if (exception != null) {\n        // Memory barrier to ensure that the decoder exception is visible from the playback thread.\n        synchronized (lock) {}\n        return false;\n      }\n    }\n\n    synchronized (lock) {\n      if (flushed) {\n        outputBuffer.release();\n      } else if (outputBuffer.isDecodeOnly()) {\n        skippedOutputBufferCount++;\n        outputBuffer.release();\n      } else {\n        outputBuffer.skippedOutputBufferCount = skippedOutputBufferCount;\n        skippedOutputBufferCount = 0;\n        queuedOutputBuffers.addLast(outputBuffer);\n      }\n      // Make the input buffer available again.\n      releaseInputBufferInternal(inputBuffer);\n    }\n\n    return true;\n  }\n\n  private boolean canDecodeBuffer() {\n    return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0;\n  }\n\n  private void releaseInputBufferInternal(I inputBuffer) {\n    inputBuffer.clear();\n    availableInputBuffers[availableInputBufferCount++] = inputBuffer;\n  }\n\n  private void releaseOutputBufferInternal(O outputBuffer) {\n    outputBuffer.clear();\n    availableOutputBuffers[availableOutputBufferCount++] = outputBuffer;\n  }\n\n  /**\n   * Creates a new input buffer.\n   */\n  protected abstract I createInputBuffer();\n\n  /**\n   * Creates a new output buffer.\n   */\n  protected abstract O createOutputBuffer();\n\n  /**\n   * Creates an exception to propagate for an unexpected decode error.\n   *\n   * @param error The unexpected decode error.\n   * @return The exception to propagate.\n   */\n  protected abstract E createUnexpectedDecodeException(Throwable error);\n\n  /**\n   * Decodes the {@code inputBuffer} and stores any decoded output in {@code outputBuffer}.\n   *\n   * @param inputBuffer The buffer to decode.\n   * @param outputBuffer The output buffer to store decoded data. The flag {@link\n   *     C#BUFFER_FLAG_DECODE_ONLY} will be set if the same flag is set on {@code inputBuffer}, but\n   *     may be set/unset as required. If the flag is set when the call returns then the output\n   *     buffer will not be made available to dequeue. The output buffer may not have been populated\n   *     in this case.\n   * @param reset Whether the decoder must be reset before decoding.\n   * @return A decoder exception if an error occurred, or null if decoding was successful.\n   */\n  @Nullable\n  protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.decoder;\n\nimport androidx.annotation.Nullable;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * Buffer for {@link SimpleDecoder} output.\n */\npublic class SimpleOutputBuffer extends OutputBuffer {\n\n  private final SimpleDecoder<?, SimpleOutputBuffer, ?> owner;\n\n  @Nullable public ByteBuffer data;\n\n  public SimpleOutputBuffer(SimpleDecoder<?, SimpleOutputBuffer, ?> owner) {\n    this.owner = owner;\n  }\n\n  /**\n   * Initializes the buffer.\n   *\n   * @param timeUs The presentation timestamp for the buffer, in microseconds.\n   * @param size An upper bound on the size of the data that will be written to the buffer.\n   * @return The {@link #data} buffer, for convenience.\n   */\n  public ByteBuffer init(long timeUs, int size) {\n    this.timeUs = timeUs;\n    if (data == null || data.capacity() < size) {\n      data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());\n    }\n    data.position(0);\n    data.limit(size);\n    return data;\n  }\n\n  @Override\n  public void clear() {\n    super.clear();\n    if (data != null) {\n      data.clear();\n    }\n  }\n\n  @Override\n  public void release() {\n    owner.releaseOutputBuffer(this);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/decoder/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.decoder;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\n/**\n * Utility methods for ClearKey.\n */\n/* package */ final class ClearKeyUtil {\n\n  private static final String TAG = \"ClearKeyUtil\";\n\n  private ClearKeyUtil() {}\n\n  /**\n   * Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant.\n   *\n   * @param request The request data.\n   * @return The adjusted request data.\n   */\n  public static byte[] adjustRequestData(byte[] request) {\n    if (Util.SDK_INT >= 27) {\n      return request;\n    }\n    // Prior to O-MR1 the ClearKey CDM encoded the values in the \"kids\" array using Base64 encoding\n    // rather than Base64Url encoding. See [Internal: b/64388098]. We know the exact request format\n    // from the platform's InitDataParser.cpp. Since there aren't any \"+\" or \"/\" symbols elsewhere\n    // in the request, it's safe to fix the encoding by replacement through the whole request.\n    String requestString = Util.fromUtf8Bytes(request);\n    return Util.getUtf8Bytes(base64ToBase64Url(requestString));\n  }\n\n  /**\n   * Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM.\n   *\n   * @param response The response data.\n   * @return The adjusted response data.\n   */\n  public static byte[] adjustResponseData(byte[] response) {\n    if (Util.SDK_INT >= 27) {\n      return response;\n    }\n    // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for\n    // the \"k\" and \"kid\" strings. See [Internal: b/64388098]. We know that the ClearKey CDM only\n    // looks at the k, kid and kty parameters in each key, so can ignore the rest of the response.\n    try {\n      JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response));\n      StringBuilder adjustedResponseBuilder = new StringBuilder(\"{\\\"keys\\\":[\");\n      JSONArray keysArray = responseJson.getJSONArray(\"keys\");\n      for (int i = 0; i < keysArray.length(); i++) {\n        if (i != 0) {\n          adjustedResponseBuilder.append(\",\");\n        }\n        JSONObject key = keysArray.getJSONObject(i);\n        adjustedResponseBuilder.append(\"{\\\"k\\\":\\\"\");\n        adjustedResponseBuilder.append(base64UrlToBase64(key.getString(\"k\")));\n        adjustedResponseBuilder.append(\"\\\",\\\"kid\\\":\\\"\");\n        adjustedResponseBuilder.append(base64UrlToBase64(key.getString(\"kid\")));\n        adjustedResponseBuilder.append(\"\\\",\\\"kty\\\":\\\"\");\n        adjustedResponseBuilder.append(key.getString(\"kty\"));\n        adjustedResponseBuilder.append(\"\\\"}\");\n      }\n      adjustedResponseBuilder.append(\"]}\");\n      return Util.getUtf8Bytes(adjustedResponseBuilder.toString());\n    } catch (JSONException e) {\n      Log.e(TAG, \"Failed to adjust response data: \" + Util.fromUtf8Bytes(response), e);\n      return response;\n    }\n  }\n\n  private static String base64ToBase64Url(String base64) {\n    return base64.replace('+', '-').replace('/', '_');\n  }\n\n  private static String base64UrlToBase64(String base64Url) {\n    return base64Url.replace('-', '+').replace('_', '/');\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\n/**\n * Thrown when a non-platform component fails to decrypt data.\n */\npublic class DecryptionException extends Exception {\n\n  /**\n   * A component specific error code.\n   */\n  public final int errorCode;\n\n  /**\n   * @param errorCode A component specific error code.\n   * @param message The detail message.\n   */\n  public DecryptionException(int errorCode, String message) {\n    super(message);\n    this.errorCode = errorCode;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.media.NotProvisionedException;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.os.SystemClock;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.EventDispatcher;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */\n@TargetApi(18)\n/* package */ class DefaultDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {\n\n  /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */\n  public static final class UnexpectedDrmSessionException extends IOException {\n\n    public UnexpectedDrmSessionException(Throwable cause) {\n      super(\"Unexpected \" + cause.getClass().getSimpleName() + \": \" + cause.getMessage(), cause);\n    }\n  }\n\n  /** Manages provisioning requests. */\n  public interface ProvisioningManager<T extends ExoMediaCrypto> {\n\n    /**\n     * Called when a session requires provisioning. The manager <em>may</em> call {@link\n     * #provision()} to have this session perform the provisioning operation. The manager\n     * <em>will</em> call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has\n     * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails.\n     *\n     * @param session The session.\n     */\n    void provisionRequired(DefaultDrmSession<T> session);\n\n    /**\n     * Called by a session when it fails to perform a provisioning operation.\n     *\n     * @param error The error that occurred.\n     */\n    void onProvisionError(Exception error);\n\n    /** Called by a session when it successfully completes a provisioning operation. */\n    void onProvisionCompleted();\n  }\n\n  /** Callback to be notified when the session is released. */\n  public interface ReleaseCallback<T extends ExoMediaCrypto> {\n\n    /**\n     * Called immediately after releasing session resources.\n     *\n     * @param session The session.\n     */\n    void onSessionReleased(DefaultDrmSession<T> session);\n  }\n\n  private static final String TAG = \"DefaultDrmSession\";\n\n  private static final int MSG_PROVISION = 0;\n  private static final int MSG_KEYS = 1;\n  private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60;\n\n  /** The DRM scheme datas, or null if this session uses offline keys. */\n  @Nullable public final List<SchemeData> schemeDatas;\n\n  private final ExoMediaDrm<T> mediaDrm;\n  private final ProvisioningManager<T> provisioningManager;\n  private final ReleaseCallback<T> releaseCallback;\n  private final @DefaultDrmSessionManager.Mode int mode;\n  private final boolean playClearSamplesWithoutKeys;\n  private final boolean isPlaceholderSession;\n  private final HashMap<String, String> keyRequestParameters;\n  private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n\n  /* package */ final MediaDrmCallback callback;\n  /* package */ final UUID uuid;\n  /* package */ final ResponseHandler responseHandler;\n\n  private @State int state;\n  private int referenceCount;\n  @Nullable private HandlerThread requestHandlerThread;\n  @Nullable private RequestHandler requestHandler;\n  @Nullable private T mediaCrypto;\n  @Nullable private DrmSessionException lastException;\n  @Nullable private byte[] sessionId;\n  @MonotonicNonNull private byte[] offlineLicenseKeySetId;\n\n  @Nullable private KeyRequest currentKeyRequest;\n  @Nullable private ProvisionRequest currentProvisionRequest;\n\n  /**\n   * Instantiates a new DRM session.\n   *\n   * @param uuid The UUID of the drm scheme.\n   * @param mediaDrm The media DRM.\n   * @param provisioningManager The manager for provisioning.\n   * @param releaseCallback The {@link ReleaseCallback}.\n   * @param schemeDatas DRM scheme datas for this session, or null if an {@code\n   *     offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true.\n   * @param mode The DRM mode. Ignored if {@code isPlaceholderSession} is true.\n   * @param isPlaceholderSession Whether this session is not expected to acquire any keys.\n   * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using\n   *     offline keys.\n   * @param keyRequestParameters Key request parameters.\n   * @param callback The media DRM callback.\n   * @param playbackLooper The playback looper.\n   * @param eventDispatcher The dispatcher for DRM session manager events.\n   * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning\n   *     requests.\n   */\n  // the constructor does not initialize fields: sessionId\n  @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n  public DefaultDrmSession(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      ProvisioningManager<T> provisioningManager,\n      ReleaseCallback<T> releaseCallback,\n      @Nullable List<SchemeData> schemeDatas,\n      @DefaultDrmSessionManager.Mode int mode,\n      boolean playClearSamplesWithoutKeys,\n      boolean isPlaceholderSession,\n      @Nullable byte[] offlineLicenseKeySetId,\n      HashMap<String, String> keyRequestParameters,\n      MediaDrmCallback callback,\n      Looper playbackLooper,\n      EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n    if (mode == DefaultDrmSessionManager.MODE_QUERY\n        || mode == DefaultDrmSessionManager.MODE_RELEASE) {\n      Assertions.checkNotNull(offlineLicenseKeySetId);\n    }\n    this.uuid = uuid;\n    this.provisioningManager = provisioningManager;\n    this.releaseCallback = releaseCallback;\n    this.mediaDrm = mediaDrm;\n    this.mode = mode;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    this.isPlaceholderSession = isPlaceholderSession;\n    if (offlineLicenseKeySetId != null) {\n      this.offlineLicenseKeySetId = offlineLicenseKeySetId;\n      this.schemeDatas = null;\n    } else {\n      this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas));\n    }\n    this.keyRequestParameters = keyRequestParameters;\n    this.callback = callback;\n    this.eventDispatcher = eventDispatcher;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    state = STATE_OPENING;\n    responseHandler = new ResponseHandler(playbackLooper);\n  }\n\n  public boolean hasSessionId(byte[] sessionId) {\n    return Arrays.equals(this.sessionId, sessionId);\n  }\n\n  public void onMediaDrmEvent(int what) {\n    switch (what) {\n      case ExoMediaDrm.EVENT_KEY_REQUIRED:\n        onKeysRequired();\n        break;\n      default:\n        break;\n    }\n  }\n\n  // Provisioning implementation.\n\n  public void provision() {\n    currentProvisionRequest = mediaDrm.getProvisionRequest();\n    Util.castNonNull(requestHandler)\n        .post(\n            MSG_PROVISION,\n            Assertions.checkNotNull(currentProvisionRequest),\n            /* allowRetry= */ true);\n  }\n\n  public void onProvisionCompleted() {\n    if (openInternal(false)) {\n      doLicense(true);\n    }\n  }\n\n  public void onProvisionError(Exception error) {\n    onError(error);\n  }\n\n  // DrmSession implementation.\n\n  @Override\n  @State\n  public final int getState() {\n    return state;\n  }\n\n  @Override\n  public boolean playClearSamplesWithoutKeys() {\n    return playClearSamplesWithoutKeys;\n  }\n\n  @Override\n  public final @Nullable DrmSessionException getError() {\n    return state == STATE_ERROR ? lastException : null;\n  }\n\n  @Override\n  public final @Nullable T getMediaCrypto() {\n    return mediaCrypto;\n  }\n\n  @Override\n  @Nullable\n  public Map<String, String> queryKeyStatus() {\n    return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);\n  }\n\n  @Override\n  @Nullable\n  public byte[] getOfflineLicenseKeySetId() {\n    return offlineLicenseKeySetId;\n  }\n\n  @Override\n  public void acquire() {\n    Assertions.checkState(referenceCount >= 0);\n    if (++referenceCount == 1) {\n      Assertions.checkState(state == STATE_OPENING);\n      requestHandlerThread = new HandlerThread(\"DrmRequestHandler\");\n      requestHandlerThread.start();\n      requestHandler = new RequestHandler(requestHandlerThread.getLooper());\n      if (openInternal(true)) {\n        doLicense(true);\n      }\n    }\n  }\n\n  @Override\n  public void release() {\n    if (--referenceCount == 0) {\n      // Assigning null to various non-null variables for clean-up.\n      state = STATE_RELEASED;\n      Util.castNonNull(responseHandler).removeCallbacksAndMessages(null);\n      Util.castNonNull(requestHandler).removeCallbacksAndMessages(null);\n      requestHandler = null;\n      Util.castNonNull(requestHandlerThread).quit();\n      requestHandlerThread = null;\n      mediaCrypto = null;\n      lastException = null;\n      currentKeyRequest = null;\n      currentProvisionRequest = null;\n      if (sessionId != null) {\n        mediaDrm.closeSession(sessionId);\n        sessionId = null;\n        eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased);\n      }\n      releaseCallback.onSessionReleased(this);\n    }\n  }\n\n  // Internal methods.\n\n  /**\n   * Try to open a session, do provisioning if necessary.\n   *\n   * @param allowProvisioning if provisioning is allowed, set this to false when calling from\n   *     processing provision response.\n   * @return true on success, false otherwise.\n   */\n  @EnsuresNonNullIf(result = true, expression = \"sessionId\")\n  private boolean openInternal(boolean allowProvisioning) {\n    if (isOpen()) {\n      // Already opened\n      return true;\n    }\n\n    try {\n      sessionId = mediaDrm.openSession();\n      mediaCrypto = mediaDrm.createMediaCrypto(sessionId);\n      eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired);\n      state = STATE_OPENED;\n      Assertions.checkNotNull(sessionId);\n      return true;\n    } catch (NotProvisionedException e) {\n      if (allowProvisioning) {\n        provisioningManager.provisionRequired(this);\n      } else {\n        onError(e);\n      }\n    } catch (Exception e) {\n      onError(e);\n    }\n\n    return false;\n  }\n\n  private void onProvisionResponse(Object request, Object response) {\n    if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) {\n      // This event is stale.\n      return;\n    }\n    currentProvisionRequest = null;\n\n    if (response instanceof Exception) {\n      provisioningManager.onProvisionError((Exception) response);\n      return;\n    }\n\n    try {\n      mediaDrm.provideProvisionResponse((byte[]) response);\n    } catch (Exception e) {\n      provisioningManager.onProvisionError(e);\n      return;\n    }\n\n    provisioningManager.onProvisionCompleted();\n  }\n\n  @RequiresNonNull(\"sessionId\")\n  private void doLicense(boolean allowRetry) {\n    if (isPlaceholderSession) {\n      return;\n    }\n    byte[] sessionId = Util.castNonNull(this.sessionId);\n    switch (mode) {\n      case DefaultDrmSessionManager.MODE_PLAYBACK:\n      case DefaultDrmSessionManager.MODE_QUERY:\n        if (offlineLicenseKeySetId == null) {\n          postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry);\n        } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) {\n          long licenseDurationRemainingSec = getLicenseDurationRemainingSec();\n          if (mode == DefaultDrmSessionManager.MODE_PLAYBACK\n              && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) {\n            Log.d(\n                TAG,\n                \"Offline license has expired or will expire soon. \"\n                    + \"Remaining seconds: \"\n                    + licenseDurationRemainingSec);\n            postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);\n          } else if (licenseDurationRemainingSec <= 0) {\n            onError(new KeysExpiredException());\n          } else {\n            state = STATE_OPENED_WITH_KEYS;\n            eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);\n          }\n        }\n        break;\n      case DefaultDrmSessionManager.MODE_DOWNLOAD:\n        if (offlineLicenseKeySetId == null || restoreKeys()) {\n          postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);\n        }\n        break;\n      case DefaultDrmSessionManager.MODE_RELEASE:\n        Assertions.checkNotNull(offlineLicenseKeySetId);\n        Assertions.checkNotNull(this.sessionId);\n        // It's not necessary to restore the key (and open a session to do that) before releasing it\n        // but this serves as a good sanity/fast-failure check.\n        if (restoreKeys()) {\n          postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);\n        }\n        break;\n      default:\n        break;\n    }\n  }\n\n  @RequiresNonNull({\"sessionId\", \"offlineLicenseKeySetId\"})\n  private boolean restoreKeys() {\n    try {\n      mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);\n      return true;\n    } catch (Exception e) {\n      Log.e(TAG, \"Error trying to restore keys.\", e);\n      onError(e);\n    }\n    return false;\n  }\n\n  private long getLicenseDurationRemainingSec() {\n    if (!C.WIDEVINE_UUID.equals(uuid)) {\n      return Long.MAX_VALUE;\n    }\n    Pair<Long, Long> pair =\n        Assertions.checkNotNull(WidevineUtil.getLicenseDurationRemainingSec(this));\n    return Math.min(pair.first, pair.second);\n  }\n\n  private void postKeyRequest(byte[] scope, int type, boolean allowRetry) {\n    try {\n      currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters);\n      Util.castNonNull(requestHandler)\n          .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry);\n    } catch (Exception e) {\n      onKeysError(e);\n    }\n  }\n\n  private void onKeyResponse(Object request, Object response) {\n    if (request != currentKeyRequest || !isOpen()) {\n      // This event is stale.\n      return;\n    }\n    currentKeyRequest = null;\n\n    if (response instanceof Exception) {\n      onKeysError((Exception) response);\n      return;\n    }\n\n    try {\n      byte[] responseData = (byte[]) response;\n      if (mode == DefaultDrmSessionManager.MODE_RELEASE) {\n        mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData);\n        eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);\n      } else {\n        byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);\n        if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD\n                || (mode == DefaultDrmSessionManager.MODE_PLAYBACK\n                    && offlineLicenseKeySetId != null))\n            && keySetId != null\n            && keySetId.length != 0) {\n          offlineLicenseKeySetId = keySetId;\n        }\n        state = STATE_OPENED_WITH_KEYS;\n        eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded);\n      }\n    } catch (Exception e) {\n      onKeysError(e);\n    }\n  }\n\n  private void onKeysRequired() {\n    if (mode == DefaultDrmSessionManager.MODE_PLAYBACK && state == STATE_OPENED_WITH_KEYS) {\n      Util.castNonNull(sessionId);\n      doLicense(/* allowRetry= */ false);\n    }\n  }\n\n  private void onKeysError(Exception e) {\n    if (e instanceof NotProvisionedException) {\n      provisioningManager.provisionRequired(this);\n    } else {\n      onError(e);\n    }\n  }\n\n  private void onError(final Exception e) {\n    lastException = new DrmSessionException(e);\n    eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e));\n    if (state != STATE_OPENED_WITH_KEYS) {\n      state = STATE_ERROR;\n    }\n  }\n\n  @EnsuresNonNullIf(result = true, expression = \"sessionId\")\n  @SuppressWarnings(\"contracts.conditional.postcondition.not.satisfied\")\n  private boolean isOpen() {\n    return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;\n  }\n\n  // Internal classes.\n\n  @SuppressLint(\"HandlerLeak\")\n  private class ResponseHandler extends Handler {\n\n    public ResponseHandler(Looper looper) {\n      super(looper);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void handleMessage(Message msg) {\n      Pair<Object, Object> requestAndResponse = (Pair<Object, Object>) msg.obj;\n      Object request = requestAndResponse.first;\n      Object response = requestAndResponse.second;\n      switch (msg.what) {\n        case MSG_PROVISION:\n          onProvisionResponse(request, response);\n          break;\n        case MSG_KEYS:\n          onKeyResponse(request, response);\n          break;\n        default:\n          break;\n      }\n    }\n  }\n\n  @SuppressLint(\"HandlerLeak\")\n  private class RequestHandler extends Handler {\n\n    public RequestHandler(Looper backgroundLooper) {\n      super(backgroundLooper);\n    }\n\n    void post(int what, Object request, boolean allowRetry) {\n      RequestTask requestTask =\n          new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request);\n      obtainMessage(what, requestTask).sendToTarget();\n    }\n\n    @Override\n    public void handleMessage(Message msg) {\n      RequestTask requestTask = (RequestTask) msg.obj;\n      Object response;\n      try {\n        switch (msg.what) {\n          case MSG_PROVISION:\n            response =\n                callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request);\n            break;\n          case MSG_KEYS:\n            response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);\n            break;\n          default:\n            throw new RuntimeException();\n        }\n      } catch (Exception e) {\n        if (maybeRetryRequest(msg, e)) {\n          return;\n        }\n        response = e;\n      }\n      responseHandler\n          .obtainMessage(msg.what, Pair.create(requestTask.request, response))\n          .sendToTarget();\n    }\n\n    private boolean maybeRetryRequest(Message originalMsg, Exception e) {\n      RequestTask requestTask = (RequestTask) originalMsg.obj;\n      if (!requestTask.allowRetry) {\n        return false;\n      }\n      requestTask.errorCount++;\n      if (requestTask.errorCount\n          > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) {\n        return false;\n      }\n      IOException ioException =\n          e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e);\n      long retryDelayMs =\n          loadErrorHandlingPolicy.getRetryDelayMsFor(\n              C.DATA_TYPE_DRM,\n              /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs,\n              ioException,\n              requestTask.errorCount);\n      if (retryDelayMs == C.TIME_UNSET) {\n        // The error is fatal.\n        return false;\n      }\n      sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs);\n      return true;\n    }\n  }\n\n  private static final class RequestTask {\n\n    public final boolean allowRetry;\n    public final long startTimeMs;\n    public final Object request;\n    public int errorCount;\n\n    public RequestTask(boolean allowRetry, long startTimeMs, Object request) {\n      this.allowRetry = allowRetry;\n      this.startTimeMs = startTimeMs;\n      this.request = request;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport com.google.android.exoplayer2.Player;\n\n/** Listener of {@link DefaultDrmSessionManager} events. */\npublic interface DefaultDrmSessionEventListener {\n\n  /** Called each time a drm session is acquired. */\n  default void onDrmSessionAcquired() {}\n\n  /** Called each time keys are loaded. */\n  default void onDrmKeysLoaded() {}\n\n  /**\n   * Called when a drm error occurs.\n   *\n   * <p>This method being called does not indicate that playback has failed, or that it will fail.\n   * The player may be able to recover from the error and continue. Hence applications should\n   * <em>not</em> implement this method to display a user visible error or initiate an application\n   * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement\n   * such behavior). This method is called to provide the application with an opportunity to log the\n   * error if it wishes to do so.\n   *\n   * @param error The corresponding exception.\n   */\n  default void onDrmSessionManagerError(Exception error) {}\n\n  /** Called each time offline keys are restored. */\n  default void onDrmKeysRestored() {}\n\n  /** Called each time offline keys are removed. */\n  default void onDrmKeysRemoved() {}\n\n  /** Called each time a drm session is released. */\n  default void onDrmSessionReleased() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.EventDispatcher;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */\n@TargetApi(18)\npublic class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {\n\n  /**\n   * Builder for {@link DefaultDrmSessionManager} instances.\n   *\n   * <p>See {@link #Builder} for the list of default values.\n   */\n  public static final class Builder {\n\n    private final HashMap<String, String> keyRequestParameters;\n    private UUID uuid;\n    private ExoMediaDrm.Provider<ExoMediaCrypto> exoMediaDrmProvider;\n    private boolean multiSession;\n    private int[] useDrmSessionsForClearContentTrackTypes;\n    private boolean playClearSamplesWithoutKeys;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n\n    /**\n     * Creates a builder with default values. The default values are:\n     *\n     * <ul>\n     *   <li>{@link #setKeyRequestParameters keyRequestParameters}: An empty map.\n     *   <li>{@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}.\n     *   <li>{@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link\n     *       FrameworkMediaDrm#DEFAULT_PROVIDER}.\n     *   <li>{@link #setMultiSession multiSession}: {@code false}.\n     *   <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.\n     *   <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}.\n     *   <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link\n     *       DefaultLoadErrorHandlingPolicy}.\n     * </ul>\n     */\n    @SuppressWarnings(\"unchecked\")\n    public Builder() {\n      keyRequestParameters = new HashMap<>();\n      uuid = C.WIDEVINE_UUID;\n      exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER;\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      useDrmSessionsForClearContentTrackTypes = new int[0];\n    }\n\n    /**\n     * Sets the key request parameters to pass as the last argument to {@link\n     * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}.\n     *\n     * <p>Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}.\n     *\n     * @param keyRequestParameters A map with parameters.\n     * @return This builder.\n     */\n    public Builder setKeyRequestParameters(Map<String, String> keyRequestParameters) {\n      this.keyRequestParameters.clear();\n      this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters));\n      return this;\n    }\n\n    /**\n     * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use.\n     *\n     * @param uuid The UUID of the DRM scheme.\n     * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}.\n     * @return This builder.\n     */\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    public Builder setUuidAndExoMediaDrmProvider(\n        UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {\n      this.uuid = Assertions.checkNotNull(uuid);\n      this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);\n      return this;\n    }\n\n    /**\n     * Sets whether this session manager is allowed to acquire multiple simultaneous sessions.\n     *\n     * <p>Users should pass false when a single key request will obtain all keys required to decrypt\n     * the associated content. {@code multiSession} is required when content uses key rotation.\n     *\n     * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous\n     *     sessions.\n     * @return This builder.\n     */\n    public Builder setMultiSession(boolean multiSession) {\n      this.multiSession = multiSession;\n      return this;\n    }\n\n    /**\n     * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear\n     * sections of the media content.\n     *\n     * <p>Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders\n     * when transitioning between clear and encrypted sections of content.\n     *\n     * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO}\n     *     and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of\n     *     whether the content is clear or encrypted.\n     * @return This builder.\n     * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains\n     *     track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}.\n     */\n    public Builder setUseDrmSessionsForClearContent(\n        int... useDrmSessionsForClearContentTrackTypes) {\n      for (int trackType : useDrmSessionsForClearContentTrackTypes) {\n        Assertions.checkArgument(\n            trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);\n      }\n      this.useDrmSessionsForClearContentTrackTypes =\n          useDrmSessionsForClearContentTrackTypes.clone();\n      return this;\n    }\n\n    /**\n     * Sets whether clear samples within protected content should be played when keys for the\n     * encrypted part of the content have yet to be loaded.\n     *\n     * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be\n     *     played when keys for the encrypted part of the content have yet to be loaded.\n     * @return This builder.\n     */\n    public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) {\n      this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n      return this;\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This builder.\n     */\n    public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);\n      return this;\n    }\n\n    /** Builds a {@link DefaultDrmSessionManager} instance. */\n    public DefaultDrmSessionManager<ExoMediaCrypto> build(MediaDrmCallback mediaDrmCallback) {\n      return new DefaultDrmSessionManager<>(\n          uuid,\n          exoMediaDrmProvider,\n          mediaDrmCallback,\n          keyRequestParameters,\n          multiSession,\n          useDrmSessionsForClearContentTrackTypes,\n          playClearSamplesWithoutKeys,\n          loadErrorHandlingPolicy);\n    }\n  }\n\n  /**\n   * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does\n   * not contain scheme data for the required UUID.\n   */\n  public static final class MissingSchemeDataException extends Exception {\n\n    private MissingSchemeDataException(UUID uuid) {\n      super(\"Media does not support uuid: \" + uuid);\n    }\n  }\n\n  /**\n   * A key for specifying PlayReady custom data in the key request parameters passed to {@link\n   * Builder#setKeyRequestParameters(Map)}.\n   */\n  public static final String PLAYREADY_CUSTOM_DATA_KEY = \"PRCustomData\";\n\n  /**\n   * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK},\n   * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})\n  public @interface Mode {}\n  /**\n   * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline\n   * licenses.\n   */\n  public static final int MODE_PLAYBACK = 0;\n  /** Restores an offline license to allow its status to be queried. */\n  public static final int MODE_QUERY = 1;\n  /** Downloads an offline license or renews an existing one. */\n  public static final int MODE_DOWNLOAD = 2;\n  /** Releases an existing offline license. */\n  public static final int MODE_RELEASE = 3;\n  /** Number of times to retry for initial provisioning and key request for reporting error. */\n  public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;\n\n  private static final String TAG = \"DefaultDrmSessionMgr\";\n\n  private final UUID uuid;\n  private final ExoMediaDrm.Provider<T> exoMediaDrmProvider;\n  private final MediaDrmCallback callback;\n  private final HashMap<String, String> keyRequestParameters;\n  private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;\n  private final boolean multiSession;\n  private final int[] useDrmSessionsForClearContentTrackTypes;\n  private final boolean playClearSamplesWithoutKeys;\n  private final ProvisioningManagerImpl provisioningManagerImpl;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n\n  private final List<DefaultDrmSession<T>> sessions;\n  private final List<DefaultDrmSession<T>> provisioningSessions;\n\n  private int prepareCallsCount;\n  @Nullable private ExoMediaDrm<T> exoMediaDrm;\n  @Nullable private DefaultDrmSession<T> placeholderDrmSession;\n  @Nullable private DefaultDrmSession<T> noMultiSessionDrmSession;\n  @Nullable private Looper playbackLooper;\n  private int mode;\n  @Nullable private byte[] offlineLicenseKeySetId;\n\n  /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\n   * @param callback Performs key and provisioning requests.\n   * @param keyRequestParameters An optional map of parameters to pass as the last argument to\n   *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   * @deprecated Use {@link Builder} instead.\n   */\n  @SuppressWarnings(\"deprecation\")\n  @Deprecated\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> exoMediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> keyRequestParameters) {\n    this(\n        uuid,\n        exoMediaDrm,\n        callback,\n        keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,\n        /* multiSession= */ false,\n        INITIAL_DRM_REQUEST_RETRY_COUNT);\n  }\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\n   * @param callback Performs key and provisioning requests.\n   * @param keyRequestParameters An optional map of parameters to pass as the last argument to\n   *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   * @param multiSession A boolean that specify whether multiple key session support is enabled.\n   *     Default is false.\n   * @deprecated Use {@link Builder} instead.\n   */\n  @Deprecated\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> exoMediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> keyRequestParameters,\n      boolean multiSession) {\n    this(\n        uuid,\n        exoMediaDrm,\n        callback,\n        keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,\n        multiSession,\n        INITIAL_DRM_REQUEST_RETRY_COUNT);\n  }\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\n   * @param callback Performs key and provisioning requests.\n   * @param keyRequestParameters An optional map of parameters to pass as the last argument to\n   *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   * @param multiSession A boolean that specify whether multiple key session support is enabled.\n   *     Default is false.\n   * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and\n   *     key request before reporting error.\n   * @deprecated Use {@link Builder} instead.\n   */\n  @Deprecated\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> exoMediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> keyRequestParameters,\n      boolean multiSession,\n      int initialDrmRequestRetryCount) {\n    this(\n        uuid,\n        new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm),\n        callback,\n        keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,\n        multiSession,\n        /* useDrmSessionsForClearContentTrackTypes= */ new int[0],\n        /* playClearSamplesWithoutKeys= */ false,\n        new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount));\n  }\n\n  // the constructor does not initialize fields: offlineLicenseKeySetId\n  @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n  private DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm.Provider<T> exoMediaDrmProvider,\n      MediaDrmCallback callback,\n      HashMap<String, String> keyRequestParameters,\n      boolean multiSession,\n      int[] useDrmSessionsForClearContentTrackTypes,\n      boolean playClearSamplesWithoutKeys,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n    Assertions.checkNotNull(uuid);\n    Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), \"Use C.CLEARKEY_UUID instead\");\n    this.uuid = uuid;\n    this.exoMediaDrmProvider = exoMediaDrmProvider;\n    this.callback = callback;\n    this.keyRequestParameters = keyRequestParameters;\n    this.eventDispatcher = new EventDispatcher<>();\n    this.multiSession = multiSession;\n    this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    provisioningManagerImpl = new ProvisioningManagerImpl();\n    mode = MODE_PLAYBACK;\n    sessions = new ArrayList<>();\n    provisioningSessions = new ArrayList<>();\n  }\n\n  /**\n   * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events.\n   *\n   * @param handler A handler to use when delivering events to {@code eventListener}.\n   * @param eventListener A listener of events.\n   */\n  public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {\n    eventDispatcher.addListener(handler, eventListener);\n  }\n\n  /**\n   * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners.\n   *\n   * @param eventListener The listener to remove.\n   */\n  public final void removeListener(DefaultDrmSessionEventListener eventListener) {\n    eventDispatcher.removeListener(eventListener);\n  }\n\n  /**\n   * Sets the mode, which determines the role of sessions acquired from the instance. This must be\n   * called before {@link #acquireSession(Looper, DrmInitData)} or {@link\n   * #acquirePlaceholderSession} is called.\n   *\n   * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when\n   * required.\n   *\n   * <p>{@code mode} must be one of these:\n   *\n   * <ul>\n   *   <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is\n   *       requested otherwise the offline license is restored.\n   *   <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license\n   *       is restored.\n   *   <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is\n   *       requested otherwise the offline license is renewed.\n   *   <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline\n   *       license is released.\n   * </ul>\n   *\n   * @param mode The mode to be set.\n   * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.\n   */\n  public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {\n    Assertions.checkState(sessions.isEmpty());\n    if (mode == MODE_QUERY || mode == MODE_RELEASE) {\n      Assertions.checkNotNull(offlineLicenseKeySetId);\n    }\n    this.mode = mode;\n    this.offlineLicenseKeySetId = offlineLicenseKeySetId;\n  }\n\n  // DrmSessionManager implementation.\n\n  @Override\n  public final void prepare() {\n    if (prepareCallsCount++ == 0) {\n      Assertions.checkState(exoMediaDrm == null);\n      exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);\n      exoMediaDrm.setOnEventListener(new MediaDrmEventListener());\n    }\n  }\n\n  @Override\n  public final void release() {\n    if (--prepareCallsCount == 0) {\n      Assertions.checkNotNull(exoMediaDrm).release();\n      exoMediaDrm = null;\n    }\n  }\n\n  @Override\n  public boolean canAcquireSession(DrmInitData drmInitData) {\n    if (offlineLicenseKeySetId != null) {\n      // An offline license can be restored so a session can always be acquired.\n      return true;\n    }\n    List<SchemeData> schemeDatas = getSchemeDatas(drmInitData, uuid, true);\n    if (schemeDatas.isEmpty()) {\n      if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) {\n        // Assume scheme specific data will be added before the session is opened.\n        Log.w(\n            TAG, \"DrmInitData only contains common PSSH SchemeData. Assuming support for: \" + uuid);\n      } else {\n        // No data for this manager's scheme.\n        return false;\n      }\n    }\n    String schemeType = drmInitData.schemeType;\n    if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {\n      // If there is no scheme information, assume patternless AES-CTR.\n      return true;\n    } else if (C.CENC_TYPE_cbc1.equals(schemeType)\n        || C.CENC_TYPE_cbcs.equals(schemeType)\n        || C.CENC_TYPE_cens.equals(schemeType)) {\n      // API support for AES-CBC and pattern encryption was added in API 24. However, the\n      // implementation was not stable until API 25.\n      return Util.SDK_INT >= 25;\n    }\n    // Unknown schemes, assume one of them is supported.\n    return true;\n  }\n\n  @Override\n  @Nullable\n  public DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {\n    assertExpectedPlaybackLooper(playbackLooper);\n    ExoMediaDrm<T> exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);\n    boolean avoidPlaceholderDrmSessions =\n        FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())\n            && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;\n    // Avoid attaching a session to sparse formats.\n    if (avoidPlaceholderDrmSessions\n        || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET\n        || exoMediaDrm.getExoMediaCryptoType() == null) {\n      return null;\n    }\n    maybeCreateMediaDrmHandler(playbackLooper);\n    if (placeholderDrmSession == null) {\n      DefaultDrmSession<T> placeholderDrmSession =\n          createNewDefaultSession(\n              /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);\n      sessions.add(placeholderDrmSession);\n      this.placeholderDrmSession = placeholderDrmSession;\n    }\n    placeholderDrmSession.acquire();\n    return placeholderDrmSession;\n  }\n\n  @Override\n  public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {\n    assertExpectedPlaybackLooper(playbackLooper);\n    maybeCreateMediaDrmHandler(playbackLooper);\n\n    @Nullable List<SchemeData> schemeDatas = null;\n    if (offlineLicenseKeySetId == null) {\n      schemeDatas = getSchemeDatas(drmInitData, uuid, false);\n      if (schemeDatas.isEmpty()) {\n        final MissingSchemeDataException error = new MissingSchemeDataException(uuid);\n        eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error));\n        return new ErrorStateDrmSession<>(new DrmSessionException(error));\n      }\n    }\n\n    @Nullable DefaultDrmSession<T> session;\n    if (!multiSession) {\n      session = noMultiSessionDrmSession;\n    } else {\n      // Only use an existing session if it has matching init data.\n      session = null;\n      for (DefaultDrmSession<T> existingSession : sessions) {\n        if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {\n          session = existingSession;\n          break;\n        }\n      }\n    }\n\n    if (session == null) {\n      // Create a new session.\n      session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);\n      if (!multiSession) {\n        noMultiSessionDrmSession = session;\n      }\n      sessions.add(session);\n    }\n    session.acquire();\n    return session;\n  }\n\n  @Override\n  @Nullable\n  public Class<T> getExoMediaCryptoType(DrmInitData drmInitData) {\n    return canAcquireSession(drmInitData)\n        ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType()\n        : null;\n  }\n\n  // Internal methods.\n\n  private void assertExpectedPlaybackLooper(Looper playbackLooper) {\n    Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);\n    this.playbackLooper = playbackLooper;\n  }\n\n  private void maybeCreateMediaDrmHandler(Looper playbackLooper) {\n    if (mediaDrmHandler == null) {\n      mediaDrmHandler = new MediaDrmHandler(playbackLooper);\n    }\n  }\n\n  private DefaultDrmSession<T> createNewDefaultSession(\n      @Nullable List<SchemeData> schemeDatas, boolean isPlaceholderSession) {\n    Assertions.checkNotNull(exoMediaDrm);\n    // Placeholder sessions should always play clear samples without keys.\n    boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;\n    return new DefaultDrmSession<>(\n        uuid,\n        exoMediaDrm,\n        /* provisioningManager= */ provisioningManagerImpl,\n        /* releaseCallback= */ this::onSessionReleased,\n        schemeDatas,\n        mode,\n        playClearSamplesWithoutKeys,\n        isPlaceholderSession,\n        offlineLicenseKeySetId,\n        keyRequestParameters,\n        callback,\n        Assertions.checkNotNull(playbackLooper),\n        eventDispatcher,\n        loadErrorHandlingPolicy);\n  }\n\n  private void onSessionReleased(DefaultDrmSession<T> drmSession) {\n    sessions.remove(drmSession);\n    if (placeholderDrmSession == drmSession) {\n      placeholderDrmSession = null;\n    }\n    if (noMultiSessionDrmSession == drmSession) {\n      noMultiSessionDrmSession = null;\n    }\n    if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) {\n      // Other sessions were waiting for the released session to complete a provision operation.\n      // We need to have one of those sessions perform the provision operation instead.\n      provisioningSessions.get(1).provision();\n    }\n    provisioningSessions.remove(drmSession);\n  }\n\n  /**\n   * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}.\n   *\n   * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.\n   * @param uuid The UUID.\n   * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be\n   *     returned.\n   * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is\n   *     present.\n   */\n  private static List<SchemeData> getSchemeDatas(\n      DrmInitData drmInitData, UUID uuid, boolean allowMissingData) {\n    // Look for matching scheme data (matching the Common PSSH box for ClearKey).\n    List<SchemeData> matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount);\n    for (int i = 0; i < drmInitData.schemeDataCount; i++) {\n      SchemeData schemeData = drmInitData.get(i);\n      boolean uuidMatches =\n          schemeData.matches(uuid)\n              || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID));\n      if (uuidMatches && (schemeData.data != null || allowMissingData)) {\n        matchingSchemeDatas.add(schemeData);\n      }\n    }\n    return matchingSchemeDatas;\n  }\n\n  @SuppressLint(\"HandlerLeak\")\n  private class MediaDrmHandler extends Handler {\n\n    public MediaDrmHandler(Looper looper) {\n      super(looper);\n    }\n\n    @Override\n    public void handleMessage(Message msg) {\n      byte[] sessionId = (byte[]) msg.obj;\n      if (sessionId == null) {\n        // The event is not associated with any particular session.\n        return;\n      }\n      for (DefaultDrmSession<T> session : sessions) {\n        if (session.hasSessionId(sessionId)) {\n          session.onMediaDrmEvent(msg.what);\n          return;\n        }\n      }\n    }\n  }\n\n  private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager<T> {\n    @Override\n    public void provisionRequired(DefaultDrmSession<T> session) {\n      if (provisioningSessions.contains(session)) {\n        // The session has already requested provisioning.\n        return;\n      }\n      provisioningSessions.add(session);\n      if (provisioningSessions.size() == 1) {\n        // This is the first session requesting provisioning, so have it perform the operation.\n        session.provision();\n      }\n    }\n\n    @Override\n    public void onProvisionCompleted() {\n      for (DefaultDrmSession<T> session : provisioningSessions) {\n        session.onProvisionCompleted();\n      }\n      provisioningSessions.clear();\n    }\n\n    @Override\n    public void onProvisionError(Exception error) {\n      for (DefaultDrmSession<T> session : provisioningSessions) {\n        session.onProvisionError(error);\n      }\n      provisioningSessions.clear();\n    }\n  }\n\n  private class MediaDrmEventListener implements OnEventListener<T> {\n\n    @Override\n    public void onEvent(\n        ExoMediaDrm<? extends T> md,\n        @Nullable byte[] sessionId,\n        int event,\n        int extra,\n        @Nullable byte[] data) {\n      Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * Initialization data for one or more DRM schemes.\n */\npublic final class DrmInitData implements Comparator<SchemeData>, Parcelable {\n\n  /**\n   * Merges {@link DrmInitData} obtained from a media manifest and a media stream.\n   *\n   * <p>The result is generated as follows.\n   *\n   * <ol>\n   *   <li>Include all {@link SchemeData}s from {@code manifestData} where {@link\n   *       SchemeData#hasData()} is true.\n   *   <li>Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()}\n   *       is true and for which we did not include an entry from the manifest targeting the same\n   *       UUID.\n   *   <li>If available, the scheme type from the manifest is used. If not, the scheme type from the\n   *       media is used.\n   * </ol>\n   *\n   * @param manifestData DRM session acquisition data obtained from the manifest.\n   * @param mediaData DRM session acquisition data obtained from the media.\n   * @return A {@link DrmInitData} obtained from merging a media manifest and a media stream.\n   */\n  public static @Nullable DrmInitData createSessionCreationData(\n      @Nullable DrmInitData manifestData, @Nullable DrmInitData mediaData) {\n    ArrayList<SchemeData> result = new ArrayList<>();\n    String schemeType = null;\n    if (manifestData != null) {\n      schemeType = manifestData.schemeType;\n      for (SchemeData data : manifestData.schemeDatas) {\n        if (data.hasData()) {\n          result.add(data);\n        }\n      }\n    }\n\n    if (mediaData != null) {\n      if (schemeType == null) {\n        schemeType = mediaData.schemeType;\n      }\n      int manifestDatasCount = result.size();\n      for (SchemeData data : mediaData.schemeDatas) {\n        if (data.hasData() && !containsSchemeDataWithUuid(result, manifestDatasCount, data.uuid)) {\n          result.add(data);\n        }\n      }\n    }\n\n    return result.isEmpty() ? null : new DrmInitData(schemeType, result);\n  }\n\n  private final SchemeData[] schemeDatas;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /** The protection scheme type, or null if not applicable or unknown. */\n  @Nullable public final String schemeType;\n\n  /**\n   * Number of {@link SchemeData}s.\n   */\n  public final int schemeDataCount;\n\n  /**\n   * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.\n   */\n  public DrmInitData(List<SchemeData> schemeDatas) {\n    this(null, false, schemeDatas.toArray(new SchemeData[0]));\n  }\n\n  /**\n   * @param schemeType See {@link #schemeType}.\n   * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.\n   */\n  public DrmInitData(@Nullable String schemeType, List<SchemeData> schemeDatas) {\n    this(schemeType, false, schemeDatas.toArray(new SchemeData[0]));\n  }\n\n  /**\n   * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.\n   */\n  public DrmInitData(SchemeData... schemeDatas) {\n    this(null, schemeDatas);\n  }\n\n  /**\n   * @param schemeType See {@link #schemeType}.\n   * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.\n   */\n  public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) {\n    this(schemeType, true, schemeDatas);\n  }\n\n  private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas,\n      SchemeData... schemeDatas) {\n    this.schemeType = schemeType;\n    if (cloneSchemeDatas) {\n      schemeDatas = schemeDatas.clone();\n    }\n    this.schemeDatas = schemeDatas;\n    schemeDataCount = schemeDatas.length;\n    // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched\n    // last. It's also required by the equals and hashcode implementations.\n    Arrays.sort(this.schemeDatas, this);\n  }\n\n  /* package */\n  DrmInitData(Parcel in) {\n    schemeType = in.readString();\n    schemeDatas = Util.castNonNull(in.createTypedArray(SchemeData.CREATOR));\n    schemeDataCount = schemeDatas.length;\n  }\n\n  /**\n   * Retrieves data for a given DRM scheme, specified by its UUID.\n   *\n   * @deprecated Use {@link #get(int)} and {@link SchemeData#matches(UUID)} instead.\n   * @param uuid The DRM scheme's UUID.\n   * @return The initialization data for the scheme, or null if the scheme is not supported.\n   */\n  @Deprecated\n  @Nullable\n  public SchemeData get(UUID uuid) {\n    for (SchemeData schemeData : schemeDatas) {\n      if (schemeData.matches(uuid)) {\n        return schemeData;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Retrieves the {@link SchemeData} at a given index.\n   *\n   * @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}.\n   * @return The {@link SchemeData} at the specified index.\n   */\n  public SchemeData get(int index) {\n    return schemeDatas[index];\n  }\n\n  /**\n   * Returns a copy with the specified protection scheme type.\n   *\n   * @param schemeType A protection scheme type. May be null.\n   * @return A copy with the specified protection scheme type.\n   */\n  public DrmInitData copyWithSchemeType(@Nullable String schemeType) {\n    if (Util.areEqual(this.schemeType, schemeType)) {\n      return this;\n    }\n    return new DrmInitData(schemeType, false, schemeDatas);\n  }\n\n  /**\n   * Returns an instance containing the {@link #schemeDatas} from both this and {@code other}. The\n   * {@link #schemeType} of the instances being merged must either match, or at least one scheme\n   * type must be {@code null}.\n   *\n   * @param drmInitData The instance to merge.\n   * @return The merged result.\n   */\n  public DrmInitData merge(DrmInitData drmInitData) {\n    Assertions.checkState(\n        schemeType == null\n            || drmInitData.schemeType == null\n            || TextUtils.equals(schemeType, drmInitData.schemeType));\n    String mergedSchemeType = schemeType != null ? this.schemeType : drmInitData.schemeType;\n    SchemeData[] mergedSchemeDatas =\n        Util.nullSafeArrayConcatenation(schemeDatas, drmInitData.schemeDatas);\n    return new DrmInitData(mergedSchemeType, mergedSchemeDatas);\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = (schemeType == null ? 0 : schemeType.hashCode());\n      result = 31 * result + Arrays.hashCode(schemeDatas);\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    DrmInitData other = (DrmInitData) obj;\n    return Util.areEqual(schemeType, other.schemeType)\n        && Arrays.equals(schemeDatas, other.schemeDatas);\n  }\n\n  @Override\n  public int compare(SchemeData first, SchemeData second) {\n    return C.UUID_NIL.equals(first.uuid) ? (C.UUID_NIL.equals(second.uuid) ? 0 : 1)\n        : first.uuid.compareTo(second.uuid);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(schemeType);\n    dest.writeTypedArray(schemeDatas, 0);\n  }\n\n  public static final Creator<DrmInitData> CREATOR =\n      new Creator<DrmInitData>() {\n\n    @Override\n    public DrmInitData createFromParcel(Parcel in) {\n      return new DrmInitData(in);\n    }\n\n    @Override\n    public DrmInitData[] newArray(int size) {\n      return new DrmInitData[size];\n    }\n\n  };\n\n  // Internal methods.\n\n  private static boolean containsSchemeDataWithUuid(\n      ArrayList<SchemeData> datas, int limit, UUID uuid) {\n    for (int i = 0; i < limit; i++) {\n      if (datas.get(i).uuid.equals(uuid)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Scheme initialization data.\n   */\n  public static final class SchemeData implements Parcelable {\n\n    // Lazily initialized hashcode.\n    private int hashCode;\n\n    /**\n     * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e.\n     * applies to all schemes).\n     */\n    private final UUID uuid;\n    /** The URL of the server to which license requests should be made. May be null if unknown. */\n    @Nullable public final String licenseServerUrl;\n    /** The mimeType of {@link #data}. */\n    public final String mimeType;\n    /** The initialization data. May be null for scheme support checks only. */\n    @Nullable public final byte[] data;\n\n    /**\n     * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is\n     *     universal (i.e. applies to all schemes).\n     * @param mimeType See {@link #mimeType}.\n     * @param data See {@link #data}.\n     */\n    public SchemeData(UUID uuid, String mimeType, @Nullable byte[] data) {\n      this(uuid, /* licenseServerUrl= */ null, mimeType, data);\n    }\n\n    /**\n     * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is\n     *     universal (i.e. applies to all schemes).\n     * @param licenseServerUrl See {@link #licenseServerUrl}.\n     * @param mimeType See {@link #mimeType}.\n     * @param data See {@link #data}.\n     */\n    public SchemeData(\n        UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) {\n      this.uuid = Assertions.checkNotNull(uuid);\n      this.licenseServerUrl = licenseServerUrl;\n      this.mimeType = Assertions.checkNotNull(mimeType);\n      this.data = data;\n    }\n\n    /* package */ SchemeData(Parcel in) {\n      uuid = new UUID(in.readLong(), in.readLong());\n      licenseServerUrl = in.readString();\n      mimeType = Util.castNonNull(in.readString());\n      data = in.createByteArray();\n    }\n\n    /**\n     * Returns whether this initialization data applies to the specified scheme.\n     *\n     * @param schemeUuid The scheme {@link UUID}.\n     * @return Whether this initialization data applies to the specified scheme.\n     */\n    public boolean matches(UUID schemeUuid) {\n      return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid);\n    }\n\n    /**\n     * Returns whether this {@link SchemeData} can be used to replace {@code other}.\n     *\n     * @param other A {@link SchemeData}.\n     * @return Whether this {@link SchemeData} can be used to replace {@code other}.\n     */\n    public boolean canReplace(SchemeData other) {\n      return hasData() && !other.hasData() && matches(other.uuid);\n    }\n\n    /**\n     * Returns whether {@link #data} is non-null.\n     */\n    public boolean hasData() {\n      return data != null;\n    }\n\n    /**\n     * Returns a copy of this instance with the specified data.\n     *\n     * @param data The data to include in the copy.\n     * @return The new instance.\n     */\n    public SchemeData copyWithData(@Nullable byte[] data) {\n      return new SchemeData(uuid, licenseServerUrl, mimeType, data);\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (!(obj instanceof SchemeData)) {\n        return false;\n      }\n      if (obj == this) {\n        return true;\n      }\n      SchemeData other = (SchemeData) obj;\n      return Util.areEqual(licenseServerUrl, other.licenseServerUrl)\n          && Util.areEqual(mimeType, other.mimeType)\n          && Util.areEqual(uuid, other.uuid)\n          && Arrays.equals(data, other.data);\n    }\n\n    @Override\n    public int hashCode() {\n      if (hashCode == 0) {\n        int result = uuid.hashCode();\n        result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());\n        result = 31 * result + mimeType.hashCode();\n        result = 31 * result + Arrays.hashCode(data);\n        hashCode = result;\n      }\n      return hashCode;\n    }\n\n    // Parcelable implementation.\n\n    @Override\n    public int describeContents() {\n      return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n      dest.writeLong(uuid.getMostSignificantBits());\n      dest.writeLong(uuid.getLeastSignificantBits());\n      dest.writeString(licenseServerUrl);\n      dest.writeString(mimeType);\n      dest.writeByteArray(data);\n    }\n\n    public static final Creator<SchemeData> CREATOR =\n        new Creator<SchemeData>() {\n\n      @Override\n      public SchemeData createFromParcel(Parcel in) {\n        return new SchemeData(in);\n      }\n\n      @Override\n      public SchemeData[] newArray(int size) {\n        return new SchemeData[size];\n      }\n\n    };\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.media.MediaDrm;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Map;\n\n/**\n * A DRM session.\n */\npublic interface DrmSession<T extends ExoMediaCrypto> {\n\n  /**\n   * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link\n   * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession}\n   * and {@code newSession} are the same session.\n   */\n  static <T extends ExoMediaCrypto> void replaceSession(\n          @Nullable DrmSession<T> previousSession, @Nullable DrmSession<T> newSession) {\n    if (previousSession == newSession) {\n      // Do nothing.\n      return;\n    }\n    if (newSession != null) {\n      newSession.acquire();\n    }\n    if (previousSession != null) {\n      previousSession.release();\n    }\n  }\n\n  /** Wraps the throwable which is the cause of the error state. */\n  class DrmSessionException extends IOException {\n\n    public DrmSessionException(Throwable cause) {\n      super(cause);\n    }\n\n  }\n\n  /**\n   * The state of the DRM session. One of {@link #STATE_RELEASED}, {@link #STATE_ERROR}, {@link\n   * #STATE_OPENING}, {@link #STATE_OPENED} or {@link #STATE_OPENED_WITH_KEYS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})\n  @interface State {}\n  /**\n   * The session has been released.\n   */\n  int STATE_RELEASED = 0;\n  /**\n   * The session has encountered an error. {@link #getError()} can be used to retrieve the cause.\n   */\n  int STATE_ERROR = 1;\n  /**\n   * The session is being opened.\n   */\n  int STATE_OPENING = 2;\n  /** The session is open, but does not have keys required for decryption. */\n  int STATE_OPENED = 3;\n  /** The session is open and has keys required for decryption. */\n  int STATE_OPENED_WITH_KEYS = 4;\n\n  /**\n   * Returns the current state of the session, which is one of {@link #STATE_ERROR},\n   * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and\n   * {@link #STATE_OPENED_WITH_KEYS}.\n   */\n  @State int getState();\n\n  /** Returns whether this session allows playback of clear samples prior to keys being loaded. */\n  default boolean playClearSamplesWithoutKeys() {\n    return false;\n  }\n\n  /**\n   * Returns the cause of the error state, or null if {@link #getState()} is not {@link\n   * #STATE_ERROR}.\n   */\n  @Nullable\n  DrmSessionException getError();\n\n  /**\n   * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has\n   * been opened or after it's been released.\n   */\n  @Nullable\n  T getMediaCrypto();\n\n  /**\n   * Returns a map describing the key status for the session, or null if called before the session\n   * has been opened or after it's been released.\n   *\n   * <p>Since DRM license policies vary by vendor, the specific status field names are determined by\n   * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names\n   * for a particular DRM engine plugin.\n   *\n   * @return A map describing the key status for the session, or null if called before the session\n   *     has been opened or after it's been released.\n   * @see MediaDrm#queryKeyStatus(byte[])\n   */\n  @Nullable\n  Map<String, String> queryKeyStatus();\n\n  /**\n   * Returns the key set id of the offline license loaded into this session, or null if there isn't\n   * one.\n   */\n  @Nullable\n  byte[] getOfflineLicenseKeySetId();\n\n  /**\n   * Increments the reference count. When the caller no longer needs to use the instance, it must\n   * call {@link #release()} to decrement the reference count.\n   */\n  void acquire();\n\n  /**\n   * Decrements the reference count. If the reference count drops to 0 underlying resources are\n   * released, and the instance cannot be re-used.\n   */\n  void release();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\n\n/**\n * Manages a DRM session.\n */\npublic interface DrmSessionManager<T extends ExoMediaCrypto> {\n\n  /** Returns {@link #DUMMY}. */\n  @SuppressWarnings(\"unchecked\")\n  static <T extends ExoMediaCrypto> DrmSessionManager<T> getDummyDrmSessionManager() {\n    return (DrmSessionManager<T>) DUMMY;\n  }\n\n  /** {@link DrmSessionManager} that supports no DRM schemes. */\n  DrmSessionManager<ExoMediaCrypto> DUMMY =\n      new DrmSessionManager<ExoMediaCrypto>() {\n\n        @Override\n        public boolean canAcquireSession(DrmInitData drmInitData) {\n          return false;\n        }\n\n        @Override\n        public DrmSession<ExoMediaCrypto> acquireSession(\n            Looper playbackLooper, DrmInitData drmInitData) {\n          return new ErrorStateDrmSession<>(\n              new DrmSession.DrmSessionException(\n                  new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));\n        }\n\n        @Override\n        @Nullable\n        public Class<ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData) {\n          return null;\n        }\n      };\n\n  /**\n   * Acquires any required resources.\n   *\n   * <p>{@link #release()} must be called to ensure the acquired resources are released. After\n   * releasing, an instance may be re-prepared.\n   */\n  default void prepare() {\n    // Do nothing.\n  }\n\n  /** Releases any acquired resources. */\n  default void release() {\n    // Do nothing.\n  }\n\n  /**\n   * Returns whether the manager is capable of acquiring a session for the given\n   * {@link DrmInitData}.\n   *\n   * @param drmInitData DRM initialization data.\n   * @return Whether the manager is capable of acquiring a session for the given\n   *     {@link DrmInitData}.\n   */\n  boolean canAcquireSession(DrmInitData drmInitData);\n\n  /**\n   * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference\n   * count. When the caller no longer needs to use the instance, it must call {@link\n   * DrmSession#release()} to decrement the reference count.\n   *\n   * <p>Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for\n   * playback of clear content periods. This can reduce the cost of transitioning between clear and\n   * encrypted content periods.\n   *\n   * @param playbackLooper The looper associated with the media playback thread.\n   * @param trackType The type of the track to acquire a placeholder session for. Must be one of the\n   *     {@link C}{@code .TRACK_TYPE_*} constants.\n   * @return The placeholder DRM session, or null if this DRM session manager does not support\n   *     placeholder sessions.\n   */\n  @Nullable\n  default DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {\n    return null;\n  }\n\n  /**\n   * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented\n   * reference count. When the caller no longer needs to use the instance, it must call {@link\n   * DrmSession#release()} to decrement the reference count.\n   *\n   * @param playbackLooper The looper associated with the media playback thread.\n   * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain\n   *     non-null {@link SchemeData#data}.\n   * @return The DRM session.\n   */\n  DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData);\n\n  /**\n   * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link\n   * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}.\n   */\n  @Nullable\n  Class<? extends ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.media.MediaDrmException;\nimport android.os.PersistableBundle;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/** An {@link ExoMediaDrm} that does not support any protection schemes. */\n@RequiresApi(18)\npublic final class DummyExoMediaDrm<T extends ExoMediaCrypto> implements ExoMediaDrm<T> {\n\n  /** Returns a new instance. */\n  @SuppressWarnings(\"unchecked\")\n  public static <T extends ExoMediaCrypto> DummyExoMediaDrm<T> getInstance() {\n    return (DummyExoMediaDrm<T>) new DummyExoMediaDrm<>();\n  }\n\n  @Override\n  public void setOnEventListener(OnEventListener<? super T> listener) {\n    // Do nothing.\n  }\n\n  @Override\n  public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> listener) {\n    // Do nothing.\n  }\n\n  @Override\n  public byte[] openSession() throws MediaDrmException {\n    throw new MediaDrmException(\"Attempting to open a session using a dummy ExoMediaDrm.\");\n  }\n\n  @Override\n  public void closeSession(byte[] sessionId) {\n    // Do nothing.\n  }\n\n  @Override\n  public KeyRequest getKeyRequest(\n      byte[] scope,\n      @Nullable List<DrmInitData.SchemeData> schemeDatas,\n      int keyType,\n      @Nullable HashMap<String, String> optionalParameters) {\n    // Should not be invoked. No session should exist.\n    throw new IllegalStateException();\n  }\n\n  @Nullable\n  @Override\n  public byte[] provideKeyResponse(byte[] scope, byte[] response) {\n    // Should not be invoked. No session should exist.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public ProvisionRequest getProvisionRequest() {\n    // Should not be invoked. No provision should be required.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public void provideProvisionResponse(byte[] response) {\n    // Should not be invoked. No provision should be required.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public Map<String, String> queryKeyStatus(byte[] sessionId) {\n    // Should not be invoked. No session should exist.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public void acquire() {\n    // Do nothing.\n  }\n\n  @Override\n  public void release() {\n    // Do nothing.\n  }\n\n  @Override\n  public void restoreKeys(byte[] sessionId, byte[] keySetId) {\n    // Should not be invoked. No session should exist.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  @Nullable\n  public PersistableBundle getMetrics() {\n    return null;\n  }\n\n  @Override\n  public String getPropertyString(String propertyName) {\n    return \"\";\n  }\n\n  @Override\n  public byte[] getPropertyByteArray(String propertyName) {\n    return Util.EMPTY_BYTE_ARRAY;\n  }\n\n  @Override\n  public void setPropertyString(String propertyName, String value) {\n    // Do nothing.\n  }\n\n  @Override\n  public void setPropertyByteArray(String propertyName, byte[] value) {\n    // Do nothing.\n  }\n\n  @Override\n  public T createMediaCrypto(byte[] sessionId) {\n    // Should not be invoked. No session should exist.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  @Nullable\n  public Class<T> getExoMediaCryptoType() {\n    // No ExoMediaCrypto type is supported.\n    return null;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Map;\n\n/** A {@link DrmSession} that's in a terminal error state. */\npublic final class ErrorStateDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {\n\n  private final DrmSessionException error;\n\n  public ErrorStateDrmSession(DrmSessionException error) {\n    this.error = Assertions.checkNotNull(error);\n  }\n\n  @Override\n  public int getState() {\n    return STATE_ERROR;\n  }\n\n  @Override\n  public boolean playClearSamplesWithoutKeys() {\n    return false;\n  }\n\n  @Override\n  @Nullable\n  public DrmSessionException getError() {\n    return error;\n  }\n\n  @Override\n  @Nullable\n  public T getMediaCrypto() {\n    return null;\n  }\n\n  @Override\n  @Nullable\n  public Map<String, String> queryKeyStatus() {\n    return null;\n  }\n\n  @Override\n  @Nullable\n  public byte[] getOfflineLicenseKeySetId() {\n    return null;\n  }\n\n  @Override\n  public void acquire() {\n    // Do nothing.\n  }\n\n  @Override\n  public void release() {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\n/** An opaque {@link android.media.MediaCrypto} equivalent. */\npublic interface ExoMediaCrypto {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.media.DeniedByServerException;\nimport android.media.MediaCryptoException;\nimport android.media.MediaDrm;\nimport android.media.MediaDrmException;\nimport android.media.NotProvisionedException;\nimport android.os.Handler;\nimport android.os.PersistableBundle;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n/**\n * Used to obtain keys for decrypting protected media streams. See {@link MediaDrm}.\n *\n * <h3>Reference counting</h3>\n *\n * <p>Access to an instance is managed by reference counting, where {@link #acquire()} increments\n * the reference count and {@link #release()} decrements it. When the reference count drops to 0\n * underlying resources are released, and the instance cannot be re-used.\n *\n * <p>Each new instance has an initial reference count of 1. Hence application code that creates a\n * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()}\n * when the instance is no longer required.\n */\npublic interface ExoMediaDrm<T extends ExoMediaCrypto> {\n\n  /** {@link ExoMediaDrm} instances provider. */\n  interface Provider<T extends ExoMediaCrypto> {\n\n    /**\n     * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller\n     * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement\n     * the reference count.\n     */\n    ExoMediaDrm<T> acquireExoMediaDrm(UUID uuid);\n  }\n\n  /**\n   * Provides an {@link ExoMediaDrm} instance owned by the app.\n   *\n   * <p>Note that when using this provider the app will have instantiated the {@link ExoMediaDrm}\n   * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance\n   * when it's no longer being used.\n   */\n  final class AppManagedProvider<T extends ExoMediaCrypto> implements Provider<T> {\n\n    private final ExoMediaDrm<T> exoMediaDrm;\n\n    /** Creates an instance that provides the given {@link ExoMediaDrm}. */\n    public AppManagedProvider(ExoMediaDrm<T> exoMediaDrm) {\n      this.exoMediaDrm = exoMediaDrm;\n    }\n\n    @Override\n    public ExoMediaDrm<T> acquireExoMediaDrm(UUID uuid) {\n      exoMediaDrm.acquire();\n      return exoMediaDrm;\n    }\n  }\n\n  /** @see MediaDrm#EVENT_KEY_REQUIRED */\n  @SuppressWarnings(\"InlinedApi\")\n  int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED;\n  /**\n   * @see MediaDrm#EVENT_KEY_EXPIRED\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED;\n  /**\n   * @see MediaDrm#EVENT_PROVISION_REQUIRED\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED;\n\n  /**\n   * @see MediaDrm#KEY_TYPE_STREAMING\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING;\n  /**\n   * @see MediaDrm#KEY_TYPE_OFFLINE\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE;\n  /**\n   * @see MediaDrm#KEY_TYPE_RELEASE\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE;\n\n  /**\n   * @see MediaDrm.OnEventListener\n   */\n  interface OnEventListener<T extends ExoMediaCrypto> {\n    /**\n     * Called when an event occurs that requires the app to be notified\n     *\n     * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.\n     * @param sessionId The DRM session ID on which the event occurred.\n     * @param event Indicates the event type.\n     * @param extra A secondary error code.\n     * @param data Optional byte array of data that may be associated with the event.\n     */\n    void onEvent(\n            ExoMediaDrm<? extends T> mediaDrm,\n            @Nullable byte[] sessionId,\n            int event,\n            int extra,\n            @Nullable byte[] data);\n  }\n\n  /**\n   * @see MediaDrm.OnKeyStatusChangeListener\n   */\n  interface OnKeyStatusChangeListener<T extends ExoMediaCrypto> {\n    /**\n     * Called when the keys in a session change status, such as when the license is renewed or\n     * expires.\n     *\n     * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.\n     * @param sessionId The DRM session ID on which the event occurred.\n     * @param exoKeyInformation A list of {@link KeyStatus} that contains key ID and status.\n     * @param hasNewUsableKey Whether a new key became usable.\n     */\n    void onKeyStatusChange(\n            ExoMediaDrm<? extends T> mediaDrm,\n            byte[] sessionId,\n            List<KeyStatus> exoKeyInformation,\n            boolean hasNewUsableKey);\n  }\n\n  /** @see MediaDrm.KeyStatus */\n  final class KeyStatus {\n\n    private final int statusCode;\n    private final byte[] keyId;\n\n    public KeyStatus(int statusCode, byte[] keyId) {\n      this.statusCode = statusCode;\n      this.keyId = keyId;\n    }\n\n    public int getStatusCode() {\n      return statusCode;\n    }\n\n    public byte[] getKeyId() {\n      return keyId;\n    }\n\n  }\n\n  /** @see MediaDrm.KeyRequest */\n  final class KeyRequest {\n\n    private final byte[] data;\n    private final String licenseServerUrl;\n\n    public KeyRequest(byte[] data, String licenseServerUrl) {\n      this.data = data;\n      this.licenseServerUrl = licenseServerUrl;\n    }\n\n    public byte[] getData() {\n      return data;\n    }\n\n    public String getLicenseServerUrl() {\n      return licenseServerUrl;\n    }\n\n  }\n\n  /** @see MediaDrm.ProvisionRequest */\n  final class ProvisionRequest {\n\n    private final byte[] data;\n    private final String defaultUrl;\n\n    public ProvisionRequest(byte[] data, String defaultUrl) {\n      this.data = data;\n      this.defaultUrl = defaultUrl;\n    }\n\n    public byte[] getData() {\n      return data;\n    }\n\n    public String getDefaultUrl() {\n      return defaultUrl;\n    }\n\n  }\n\n  /**\n   * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener)\n   */\n  void setOnEventListener(OnEventListener<? super T> listener);\n\n  /**\n   * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler)\n   */\n  void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> listener);\n\n  /**\n   * @see MediaDrm#openSession()\n   */\n  byte[] openSession() throws MediaDrmException;\n\n  /**\n   * @see MediaDrm#closeSession(byte[])\n   */\n  void closeSession(byte[] sessionId);\n\n  /**\n   * Generates a key request.\n   *\n   * @param scope If {@code keyType} is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE},\n   *     the session id that the keys will be provided to. If {@code keyType} is {@link\n   *     #KEY_TYPE_RELEASE}, the keySetId of the keys to release.\n   * @param schemeDatas If key type is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE}, a\n   *     list of {@link SchemeData} instances extracted from the media. Null otherwise.\n   * @param keyType The type of the request. Either {@link #KEY_TYPE_STREAMING} to acquire keys for\n   *     streaming, {@link #KEY_TYPE_OFFLINE} to acquire keys for offline usage, or {@link\n   *     #KEY_TYPE_RELEASE} to release acquired keys. Releasing keys invalidates them for all\n   *     sessions.\n   * @param optionalParameters Are included in the key request message to allow a client application\n   *     to provide additional message parameters to the server. This may be {@code null} if no\n   *     additional parameters are to be sent.\n   * @return The generated key request.\n   * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)\n   */\n  KeyRequest getKeyRequest(\n          byte[] scope,\n          @Nullable List<SchemeData> schemeDatas,\n          int keyType,\n          @Nullable HashMap<String, String> optionalParameters)\n      throws NotProvisionedException;\n\n  /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */\n  @Nullable\n  byte[] provideKeyResponse(byte[] scope, byte[] response)\n      throws NotProvisionedException, DeniedByServerException;\n\n  /**\n   * @see MediaDrm#getProvisionRequest()\n   */\n  ProvisionRequest getProvisionRequest();\n\n  /**\n   * @see MediaDrm#provideProvisionResponse(byte[])\n   */\n  void provideProvisionResponse(byte[] response) throws DeniedByServerException;\n\n  /**\n   * @see MediaDrm#queryKeyStatus(byte[])\n   */\n  Map<String, String> queryKeyStatus(byte[] sessionId);\n\n  /**\n   * Increments the reference count. When the caller no longer needs to use the instance, it must\n   * call {@link #release()} to decrement the reference count.\n   *\n   * <p>A new instance will have an initial reference count of 1, and therefore it is not normally\n   * necessary for application code to call this method.\n   */\n  void acquire();\n\n  /**\n   * Decrements the reference count. If the reference count drops to 0 underlying resources are\n   * released, and the instance cannot be re-used.\n   */\n  void release();\n\n  /**\n   * @see MediaDrm#restoreKeys(byte[], byte[])\n   */\n  void restoreKeys(byte[] sessionId, byte[] keySetId);\n\n  /**\n   * Returns drm metrics. May be null if unavailable.\n   *\n   * @see MediaDrm#getMetrics()\n   */\n  @Nullable\n  PersistableBundle getMetrics();\n\n  /**\n   * @see MediaDrm#getPropertyString(String)\n   */\n  String getPropertyString(String propertyName);\n\n  /**\n   * @see MediaDrm#getPropertyByteArray(String)\n   */\n  byte[] getPropertyByteArray(String propertyName);\n\n  /**\n   * @see MediaDrm#setPropertyString(String, String)\n   */\n  void setPropertyString(String propertyName, String value);\n\n  /**\n   * @see MediaDrm#setPropertyByteArray(String, byte[])\n   */\n  void setPropertyByteArray(String propertyName, byte[] value);\n\n  /**\n   * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])\n   * @param sessionId The DRM session ID.\n   * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.\n   * @throws MediaCryptoException If the instance can't be created.\n   */\n  T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;\n\n  /**\n   * Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}, or null\n   * if this instance cannot create any {@link ExoMediaCrypto} instances.\n   */\n  @Nullable\n  Class<T> getExoMediaCryptoType();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.media.MediaCrypto;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.UUID;\n\n/**\n * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or\n * update a framework {@link MediaCrypto}.\n */\npublic final class FrameworkMediaCrypto implements ExoMediaCrypto {\n\n  /**\n   * Whether the device needs keys to have been loaded into the {@link DrmSession} before codec\n   * configuration.\n   */\n  public static final boolean WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC =\n      \"Amazon\".equals(Util.MANUFACTURER)\n          && (\"AFTM\".equals(Util.MODEL) // Fire TV Stick Gen 1\n              || \"AFTB\".equals(Util.MODEL)); // Fire TV Gen 1\n\n  /** The DRM scheme UUID. */\n  public final UUID uuid;\n  /** The DRM session id. */\n  public final byte[] sessionId;\n  /**\n   * Whether to allow use of insecure decoder components even if the underlying platform says\n   * otherwise.\n   */\n  public final boolean forceAllowInsecureDecoderComponents;\n\n  /**\n   * @param uuid The DRM scheme UUID.\n   * @param sessionId The DRM session id.\n   * @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components\n   *     even if the underlying platform says otherwise.\n   */\n  public FrameworkMediaCrypto(\n      UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {\n    this.uuid = uuid;\n    this.sessionId = sessionId;\n    this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.media.DeniedByServerException;\nimport android.media.MediaCryptoException;\nimport android.media.MediaDrm;\nimport android.media.MediaDrmException;\nimport android.media.NotProvisionedException;\nimport android.media.UnsupportedSchemeException;\nimport android.os.PersistableBundle;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */\n@TargetApi(23)\n@RequiresApi(18)\npublic final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {\n\n  private static final String TAG = \"FrameworkMediaDrm\";\n\n  /**\n   * {@link Provider} that returns a new {@link FrameworkMediaDrm} for the requested\n   * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID\n   * is not supported by the device.\n   */\n  public static final Provider<FrameworkMediaCrypto> DEFAULT_PROVIDER =\n      uuid -> {\n        try {\n          return newInstance(uuid);\n        } catch (UnsupportedDrmException e) {\n          Log.e(TAG, \"Failed to instantiate a FrameworkMediaDrm for uuid: \" + uuid + \".\");\n          return new DummyExoMediaDrm<>();\n        }\n      };\n\n  private static final String CENC_SCHEME_MIME_TYPE = \"cenc\";\n  private static final String MOCK_LA_URL_VALUE = \"https://x\";\n  private static final String MOCK_LA_URL = \"<LA_URL>\" + MOCK_LA_URL_VALUE + \"</LA_URL>\";\n  private static final int UTF_16_BYTES_PER_CHARACTER = 2;\n\n  private final UUID uuid;\n  private final MediaDrm mediaDrm;\n  private int referenceCount;\n\n  /**\n   * Creates an instance with an initial reference count of 1. {@link #release()} must be called on\n   * the instance when it's no longer required.\n   *\n   * @param uuid The scheme uuid.\n   * @return The created instance.\n   * @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.\n   */\n  public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {\n    try {\n      return new FrameworkMediaDrm(uuid);\n    } catch (UnsupportedSchemeException e) {\n      throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);\n    } catch (Exception e) {\n      throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);\n    }\n  }\n\n  private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {\n    Assertions.checkNotNull(uuid);\n    Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), \"Use C.CLEARKEY_UUID instead\");\n    this.uuid = uuid;\n    this.mediaDrm = new MediaDrm(adjustUuid(uuid));\n    // Creators of an instance automatically acquire ownership of the created instance.\n    referenceCount = 1;\n    if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {\n      forceWidevineL3(mediaDrm);\n    }\n  }\n\n  @Override\n  public void setOnEventListener(\n      final OnEventListener<? super FrameworkMediaCrypto> listener) {\n    mediaDrm.setOnEventListener(\n        listener == null\n            ? null\n            : (mediaDrm, sessionId, event, extra, data) ->\n                listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data));\n  }\n\n  @Override\n  public void setOnKeyStatusChangeListener(\n      final OnKeyStatusChangeListener<? super FrameworkMediaCrypto> listener) {\n    if (Util.SDK_INT < 23) {\n      throw new UnsupportedOperationException();\n    }\n\n    mediaDrm.setOnKeyStatusChangeListener(\n        listener == null\n            ? null\n            : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> {\n              List<KeyStatus> exoKeyInfo = new ArrayList<>();\n              for (MediaDrm.KeyStatus keyStatus : keyInfo) {\n                exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId()));\n              }\n              listener.onKeyStatusChange(\n                  FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey);\n            },\n        null);\n  }\n\n  @Override\n  public byte[] openSession() throws MediaDrmException {\n    return mediaDrm.openSession();\n  }\n\n  @Override\n  public void closeSession(byte[] sessionId) {\n    mediaDrm.closeSession(sessionId);\n  }\n\n  @Override\n  public KeyRequest getKeyRequest(\n      byte[] scope,\n      @Nullable List<SchemeData> schemeDatas,\n      int keyType,\n      @Nullable HashMap<String, String> optionalParameters)\n      throws NotProvisionedException {\n    SchemeData schemeData = null;\n    byte[] initData = null;\n    String mimeType = null;\n    if (schemeDatas != null) {\n      schemeData = getSchemeData(uuid, schemeDatas);\n      initData = adjustRequestInitData(uuid, Assertions.checkNotNull(schemeData.data));\n      mimeType = adjustRequestMimeType(uuid, schemeData.mimeType);\n    }\n    MediaDrm.KeyRequest request =\n        mediaDrm.getKeyRequest(scope, initData, mimeType, keyType, optionalParameters);\n\n    byte[] requestData = adjustRequestData(uuid, request.getData());\n\n    String licenseServerUrl = request.getDefaultUrl();\n    if (MOCK_LA_URL_VALUE.equals(licenseServerUrl)) {\n      licenseServerUrl = \"\";\n    }\n    if (TextUtils.isEmpty(licenseServerUrl)\n        && schemeData != null\n        && !TextUtils.isEmpty(schemeData.licenseServerUrl)) {\n      licenseServerUrl = schemeData.licenseServerUrl;\n    }\n\n    return new KeyRequest(requestData, licenseServerUrl);\n  }\n\n  @Nullable\n  @Override\n  public byte[] provideKeyResponse(byte[] scope, byte[] response)\n      throws NotProvisionedException, DeniedByServerException {\n    if (C.CLEARKEY_UUID.equals(uuid)) {\n      response = ClearKeyUtil.adjustResponseData(response);\n    }\n\n    return mediaDrm.provideKeyResponse(scope, response);\n  }\n\n  @Override\n  public ProvisionRequest getProvisionRequest() {\n    final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest();\n    return new ProvisionRequest(request.getData(), request.getDefaultUrl());\n  }\n\n  @Override\n  public void provideProvisionResponse(byte[] response) throws DeniedByServerException {\n    mediaDrm.provideProvisionResponse(response);\n  }\n\n  @Override\n  public Map<String, String> queryKeyStatus(byte[] sessionId) {\n    return mediaDrm.queryKeyStatus(sessionId);\n  }\n\n  @Override\n  public synchronized void acquire() {\n    Assertions.checkState(referenceCount > 0);\n    referenceCount++;\n  }\n\n  @Override\n  public synchronized void release() {\n    if (--referenceCount == 0) {\n      mediaDrm.release();\n    }\n  }\n\n  @Override\n  public void restoreKeys(byte[] sessionId, byte[] keySetId) {\n    mediaDrm.restoreKeys(sessionId, keySetId);\n  }\n\n  @Override\n  @Nullable\n  @TargetApi(28)\n  public PersistableBundle getMetrics() {\n    if (Util.SDK_INT < 28) {\n      return null;\n    }\n    return mediaDrm.getMetrics();\n  }\n\n  @Override\n  public String getPropertyString(String propertyName) {\n    return mediaDrm.getPropertyString(propertyName);\n  }\n\n  @Override\n  public byte[] getPropertyByteArray(String propertyName) {\n    return mediaDrm.getPropertyByteArray(propertyName);\n  }\n\n  @Override\n  public void setPropertyString(String propertyName, String value) {\n    mediaDrm.setPropertyString(propertyName, value);\n  }\n\n  @Override\n  public void setPropertyByteArray(String propertyName, byte[] value) {\n    mediaDrm.setPropertyByteArray(propertyName, value);\n  }\n\n  @Override\n  public FrameworkMediaCrypto createMediaCrypto(byte[] initData) throws MediaCryptoException {\n    // Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still\n    // indicate that it required secure video decoders [Internal ref: b/11428937].\n    boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21\n        && C.WIDEVINE_UUID.equals(uuid) && \"L3\".equals(getPropertyString(\"securityLevel\"));\n    return new FrameworkMediaCrypto(\n        adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);\n  }\n\n  @Override\n  public Class<FrameworkMediaCrypto> getExoMediaCryptoType() {\n    return FrameworkMediaCrypto.class;\n  }\n\n  private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {\n    if (!C.WIDEVINE_UUID.equals(uuid)) {\n      // For non-Widevine CDMs always use the first scheme data.\n      return schemeDatas.get(0);\n    }\n\n    if (Util.SDK_INT >= 28 && schemeDatas.size() > 1) {\n      // For API level 28 and above, concatenate multiple PSSH scheme datas if possible.\n      SchemeData firstSchemeData = schemeDatas.get(0);\n      int concatenatedDataLength = 0;\n      boolean canConcatenateData = true;\n      for (int i = 0; i < schemeDatas.size(); i++) {\n        SchemeData schemeData = schemeDatas.get(i);\n        byte[] schemeDataData = Util.castNonNull(schemeData.data);\n        if (Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType)\n            && Util.areEqual(schemeData.licenseServerUrl, firstSchemeData.licenseServerUrl)\n            && PsshAtomUtil.isPsshAtom(schemeDataData)) {\n          concatenatedDataLength += schemeDataData.length;\n        } else {\n          canConcatenateData = false;\n          break;\n        }\n      }\n      if (canConcatenateData) {\n        byte[] concatenatedData = new byte[concatenatedDataLength];\n        int concatenatedDataPosition = 0;\n        for (int i = 0; i < schemeDatas.size(); i++) {\n          SchemeData schemeData = schemeDatas.get(i);\n          byte[] schemeDataData = Util.castNonNull(schemeData.data);\n          int schemeDataLength = schemeDataData.length;\n          System.arraycopy(\n              schemeDataData, 0, concatenatedData, concatenatedDataPosition, schemeDataLength);\n          concatenatedDataPosition += schemeDataLength;\n        }\n        return firstSchemeData.copyWithData(concatenatedData);\n      }\n    }\n\n    // For API levels 23 - 27, prefer the first V1 PSSH box. For API levels 22 and earlier, prefer\n    // the first V0 box.\n    for (int i = 0; i < schemeDatas.size(); i++) {\n      SchemeData schemeData = schemeDatas.get(i);\n      int version = PsshAtomUtil.parseVersion(Util.castNonNull(schemeData.data));\n      if (Util.SDK_INT < 23 && version == 0) {\n        return schemeData;\n      } else if (Util.SDK_INT >= 23 && version == 1) {\n        return schemeData;\n      }\n    }\n\n    // If all else fails, use the first scheme data.\n    return schemeDatas.get(0);\n  }\n\n  private static UUID adjustUuid(UUID uuid) {\n    // ClearKey had to be accessed using the Common PSSH UUID prior to API level 27.\n    return Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;\n  }\n\n  private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) {\n    // TODO: Add API level check once [Internal ref: b/112142048] is fixed.\n    if (C.PLAYREADY_UUID.equals(uuid)) {\n      byte[] schemeSpecificData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);\n      if (schemeSpecificData == null) {\n        // The init data is not contained in a pssh box.\n        schemeSpecificData = initData;\n      }\n      initData =\n          PsshAtomUtil.buildPsshAtom(\n              C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData));\n    }\n\n    // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon\n    // devices also required data to be extracted from the PSSH atom for PlayReady.\n    if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid))\n        || (C.PLAYREADY_UUID.equals(uuid)\n            && \"Amazon\".equals(Util.MANUFACTURER)\n            && (\"AFTB\".equals(Util.MODEL) // Fire TV Gen 1\n                || \"AFTS\".equals(Util.MODEL) // Fire TV Gen 2\n                || \"AFTM\".equals(Util.MODEL)))) { // Fire TV Stick Gen 1\n      byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);\n      if (psshData != null) {\n        // Extraction succeeded, so return the extracted data.\n        return psshData;\n      }\n    }\n    return initData;\n  }\n\n  private static String adjustRequestMimeType(UUID uuid, String mimeType) {\n    // Prior to API level 26 the ClearKey CDM only accepted \"cenc\" as the scheme for MP4.\n    if (Util.SDK_INT < 26\n        && C.CLEARKEY_UUID.equals(uuid)\n        && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {\n      return CENC_SCHEME_MIME_TYPE;\n    }\n    return mimeType;\n  }\n\n  private static byte[] adjustRequestData(UUID uuid, byte[] requestData) {\n    if (C.CLEARKEY_UUID.equals(uuid)) {\n      return ClearKeyUtil.adjustRequestData(requestData);\n    }\n    return requestData;\n  }\n\n  @SuppressLint(\"WrongConstant\") // Suppress spurious lint error [Internal ref: b/32137960]\n  private static void forceWidevineL3(MediaDrm mediaDrm) {\n    mediaDrm.setPropertyString(\"securityLevel\", \"L3\");\n  }\n\n  /**\n   * Returns whether the device codec is known to fail if security level L1 is used.\n   *\n   * <p>See <a href=\"https://github.com/google/ExoPlayer/issues/4413\">GitHub issue #4413</a>.\n   */\n  private static boolean needsForceWidevineL3Workaround() {\n    return \"ASUS_Z00AD\".equals(Util.MODEL);\n  }\n\n  /**\n   * If the LA_URL tag is missing, injects a mock LA_URL value to avoid causing the CDM to throw\n   * when creating the key request. The LA_URL attribute is optional but some Android PlayReady\n   * implementations are known to require it. Does nothing it the provided {@code data} already\n   * contains an LA_URL value.\n   */\n  private static byte[] addLaUrlAttributeIfMissing(byte[] data) {\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    // See https://docs.microsoft.com/en-us/playready/specifications/specifications for more\n    // information about the init data format.\n    int length = byteArray.readLittleEndianInt();\n    int objectRecordCount = byteArray.readLittleEndianShort();\n    int recordType = byteArray.readLittleEndianShort();\n    if (objectRecordCount != 1 || recordType != 1) {\n      Log.i(TAG, \"Unexpected record count or type. Skipping LA_URL workaround.\");\n      return data;\n    }\n    int recordLength = byteArray.readLittleEndianShort();\n    String xml = byteArray.readString(recordLength, Charset.forName(C.UTF16LE_NAME));\n    if (xml.contains(\"<LA_URL>\")) {\n      // LA_URL already present. Do nothing.\n      return data;\n    }\n    // This PlayReady object record does not include an LA_URL. We add a mock value for it.\n    int endOfDataTagIndex = xml.indexOf(\"</DATA>\");\n    if (endOfDataTagIndex == -1) {\n      Log.w(TAG, \"Could not find the </DATA> tag. Skipping LA_URL workaround.\");\n    }\n    String xmlWithMockLaUrl =\n        xml.substring(/* beginIndex= */ 0, /* endIndex= */ endOfDataTagIndex)\n            + MOCK_LA_URL\n            + xml.substring(/* beginIndex= */ endOfDataTagIndex);\n    int extraBytes = MOCK_LA_URL.length() * UTF_16_BYTES_PER_CHARACTER;\n    ByteBuffer newData = ByteBuffer.allocate(length + extraBytes);\n    newData.order(ByteOrder.LITTLE_ENDIAN);\n    newData.putInt(length + extraBytes);\n    newData.putShort((short) objectRecordCount);\n    newData.putShort((short) recordType);\n    newData.putShort((short) (xmlWithMockLaUrl.length() * UTF_16_BYTES_PER_CHARACTER));\n    newData.put(xmlWithMockLaUrl.getBytes(Charset.forName(C.UTF16LE_NAME)));\n    return newData.array();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.annotation.TargetApi;\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;\nimport com.google.android.exoplayer2.upstream.DataSourceInputStream;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n/**\n * A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances.\n */\n@TargetApi(18)\npublic final class HttpMediaDrmCallback implements MediaDrmCallback {\n\n  private static final int MAX_MANUAL_REDIRECTS = 5;\n\n  private final HttpDataSource.Factory dataSourceFactory;\n  private final String defaultLicenseUrl;\n  private final boolean forceDefaultLicenseUrl;\n  private final Map<String, String> keyRequestProperties;\n\n  /**\n   * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify\n   *     their own license URL.\n   * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.\n   */\n  public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {\n    this(defaultLicenseUrl, false, dataSourceFactory);\n  }\n\n  /**\n   * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify\n   *     their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is\n   *     set to true.\n   * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that\n   *     include their own license URL.\n   * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.\n   */\n  public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,\n      HttpDataSource.Factory dataSourceFactory) {\n    this.dataSourceFactory = dataSourceFactory;\n    this.defaultLicenseUrl = defaultLicenseUrl;\n    this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;\n    this.keyRequestProperties = new HashMap<>();\n  }\n\n  /**\n   * Sets a header for key requests made by the callback.\n   *\n   * @param name The name of the header field.\n   * @param value The value of the field.\n   */\n  public void setKeyRequestProperty(String name, String value) {\n    Assertions.checkNotNull(name);\n    Assertions.checkNotNull(value);\n    synchronized (keyRequestProperties) {\n      keyRequestProperties.put(name, value);\n    }\n  }\n\n  /**\n   * Clears a header for key requests made by the callback.\n   *\n   * @param name The name of the header field.\n   */\n  public void clearKeyRequestProperty(String name) {\n    Assertions.checkNotNull(name);\n    synchronized (keyRequestProperties) {\n      keyRequestProperties.remove(name);\n    }\n  }\n\n  /**\n   * Clears all headers for key requests made by the callback.\n   */\n  public void clearAllKeyRequestProperties() {\n    synchronized (keyRequestProperties) {\n      keyRequestProperties.clear();\n    }\n  }\n\n  @Override\n  public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {\n    String url =\n        request.getDefaultUrl() + \"&signedRequest=\" + Util.fromUtf8Bytes(request.getData());\n    return executePost(dataSourceFactory, url, /* httpBody= */ null, /* requestProperties= */ null);\n  }\n\n  @Override\n  public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {\n    String url = request.getLicenseServerUrl();\n    if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {\n      url = defaultLicenseUrl;\n    }\n    Map<String, String> requestProperties = new HashMap<>();\n    // Add standard request properties for supported schemes.\n    String contentType = C.PLAYREADY_UUID.equals(uuid) ? \"text/xml\"\n        : (C.CLEARKEY_UUID.equals(uuid) ? \"application/json\" : \"application/octet-stream\");\n    requestProperties.put(\"Content-Type\", contentType);\n    if (C.PLAYREADY_UUID.equals(uuid)) {\n      requestProperties.put(\"SOAPAction\",\n          \"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense\");\n    }\n    // Add additional request properties.\n    synchronized (keyRequestProperties) {\n      requestProperties.putAll(keyRequestProperties);\n    }\n    return executePost(dataSourceFactory, url, request.getData(), requestProperties);\n  }\n\n  private static byte[] executePost(\n      HttpDataSource.Factory dataSourceFactory,\n      String url,\n      @Nullable byte[] httpBody,\n      @Nullable Map<String, String> requestProperties)\n      throws IOException {\n    HttpDataSource dataSource = dataSourceFactory.createDataSource();\n    if (requestProperties != null) {\n      for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {\n        dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());\n      }\n    }\n\n    int manualRedirectCount = 0;\n    while (true) {\n      DataSpec dataSpec =\n          new DataSpec(\n              Uri.parse(url),\n              DataSpec.HTTP_METHOD_POST,\n              httpBody,\n              /* absoluteStreamPosition= */ 0,\n              /* position= */ 0,\n              /* length= */ C.LENGTH_UNSET,\n              /* key= */ null,\n              DataSpec.FLAG_ALLOW_GZIP);\n      DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);\n      try {\n        return Util.toByteArray(inputStream);\n      } catch (InvalidResponseCodeException e) {\n        // For POST requests, the underlying network stack will not normally follow 307 or 308\n        // redirects automatically. Do so manually here.\n        boolean manuallyRedirect =\n            (e.responseCode == 307 || e.responseCode == 308)\n                && manualRedirectCount++ < MAX_MANUAL_REDIRECTS;\n        String redirectUrl = manuallyRedirect ? getRedirectUrl(e) : null;\n        if (redirectUrl == null) {\n          throw e;\n        }\n        url = redirectUrl;\n      } finally {\n        Util.closeQuietly(inputStream);\n      }\n    }\n  }\n\n  private static @Nullable String getRedirectUrl(InvalidResponseCodeException exception) {\n    Map<String, List<String>> headerFields = exception.headerFields;\n    if (headerFields != null) {\n      List<String> locationHeaders = headerFields.get(\"Location\");\n      if (locationHeaders != null && !locationHeaders.isEmpty()) {\n        return locationHeaders.get(0);\n      }\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/KeysExpiredException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\n/**\n * Thrown when the drm keys loaded into an open session expire.\n */\npublic final class KeysExpiredException extends Exception {\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.UUID;\n\n/**\n * A {@link MediaDrmCallback} that provides a fixed response to key requests. Provisioning is not\n * supported. This implementation is primarily useful for providing locally stored keys to decrypt\n * ClearKey protected content. It is not suitable for use with Widevine or PlayReady protected\n * content.\n */\npublic final class LocalMediaDrmCallback implements MediaDrmCallback {\n\n  private final byte[] keyResponse;\n\n  /**\n   * @param keyResponse The fixed response for all key requests.\n   */\n  public LocalMediaDrmCallback(byte[] keyResponse) {\n    this.keyResponse = Assertions.checkNotNull(keyResponse);\n  }\n\n  @Override\n  public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {\n    return keyResponse;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;\nimport java.util.UUID;\n\n/**\n * Performs {@link ExoMediaDrm} key and provisioning requests.\n */\npublic interface MediaDrmCallback {\n\n  /**\n   * Executes a provisioning request.\n   *\n   * @param uuid The UUID of the content protection scheme.\n   * @param request The request.\n   * @return The response data.\n   * @throws Exception If an error occurred executing the request.\n   */\n  byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws Exception;\n\n  /**\n   * Executes a key request.\n   *\n   * @param uuid The UUID of the content protection scheme.\n   * @param request The request.\n   * @return The response data.\n   * @throws Exception If an error occurred executing the request.\n   */\n  byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaDrm;\nimport android.os.ConditionVariable;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.Factory;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\n\n/** Helper class to download, renew and release offline licenses. */\n@TargetApi(18)\n@RequiresApi(18)\npublic final class OfflineLicenseHelper<T extends ExoMediaCrypto> {\n\n  private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData();\n\n  private final ConditionVariable conditionVariable;\n  private final DefaultDrmSessionManager<T> drmSessionManager;\n  private final HandlerThread handlerThread;\n\n  /**\n   * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance\n   * is no longer required.\n   *\n   * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify\n   *     their own license URL.\n   * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.\n   * @return A new instance which uses Widevine CDM.\n   * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be\n   *     instantiated.\n   */\n  public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(\n      String defaultLicenseUrl, Factory httpDataSourceFactory)\n      throws UnsupportedDrmException {\n    return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null);\n  }\n\n  /**\n   * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance\n   * is no longer required.\n   *\n   * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify\n   *     their own license URL.\n   * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that\n   *     include their own license URL.\n   * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.\n   * @return A new instance which uses Widevine CDM.\n   * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be\n   *     instantiated.\n   */\n  public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(\n      String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory)\n      throws UnsupportedDrmException {\n    return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory,\n        null);\n  }\n\n  /**\n   * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance\n   * is no longer required.\n   *\n   * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify\n   *     their own license URL.\n   * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that\n   *     include their own license URL.\n   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument\n   *     to {@link MediaDrm#getKeyRequest}. May be null.\n   * @return A new instance which uses Widevine CDM.\n   * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be\n   *     instantiated.\n   * @see DefaultDrmSessionManager.Builder\n   */\n  public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(\n      String defaultLicenseUrl,\n      boolean forceDefaultLicenseUrl,\n      Factory httpDataSourceFactory,\n      @Nullable Map<String, String> optionalKeyRequestParameters)\n      throws UnsupportedDrmException {\n    return new OfflineLicenseHelper<>(\n        C.WIDEVINE_UUID,\n        FrameworkMediaDrm.DEFAULT_PROVIDER,\n        new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory),\n        optionalKeyRequestParameters);\n  }\n\n  /**\n   * Constructs an instance. Call {@link #release()} when the instance is no longer required.\n   *\n   * @param uuid The UUID of the drm scheme.\n   * @param mediaDrmProvider A {@link ExoMediaDrm.Provider}.\n   * @param callback Performs key and provisioning requests.\n   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument\n   *     to {@link MediaDrm#getKeyRequest}. May be null.\n   * @see DefaultDrmSessionManager.Builder\n   */\n  @SuppressWarnings(\"unchecked\")\n  public OfflineLicenseHelper(\n      UUID uuid,\n      ExoMediaDrm.Provider<T> mediaDrmProvider,\n      MediaDrmCallback callback,\n      @Nullable Map<String, String> optionalKeyRequestParameters) {\n    handlerThread = new HandlerThread(\"OfflineLicenseHelper\");\n    handlerThread.start();\n    conditionVariable = new ConditionVariable();\n    DefaultDrmSessionEventListener eventListener =\n        new DefaultDrmSessionEventListener() {\n          @Override\n          public void onDrmKeysLoaded() {\n            conditionVariable.open();\n          }\n\n          @Override\n          public void onDrmSessionManagerError(Exception e) {\n            conditionVariable.open();\n          }\n\n          @Override\n          public void onDrmKeysRestored() {\n            conditionVariable.open();\n          }\n\n          @Override\n          public void onDrmKeysRemoved() {\n            conditionVariable.open();\n          }\n        };\n    if (optionalKeyRequestParameters == null) {\n      optionalKeyRequestParameters = Collections.emptyMap();\n    }\n    drmSessionManager =\n        (DefaultDrmSessionManager<T>)\n            new DefaultDrmSessionManager.Builder()\n                .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider)\n                .setKeyRequestParameters(optionalKeyRequestParameters)\n                .build(callback);\n    drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener);\n  }\n\n  /**\n   * Downloads an offline license.\n   *\n   * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded.\n   * @return The key set id for the downloaded license.\n   * @throws DrmSessionException Thrown when a DRM session error occurs.\n   */\n  public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException {\n    Assertions.checkArgument(drmInitData != null);\n    return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);\n  }\n\n  /**\n   * Renews an offline license.\n   *\n   * @param offlineLicenseKeySetId The key set id of the license to be renewed.\n   * @return The renewed offline license key set id.\n   * @throws DrmSessionException Thrown when a DRM session error occurs.\n   */\n  public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId)\n      throws DrmSessionException {\n    Assertions.checkNotNull(offlineLicenseKeySetId);\n    return blockingKeyRequest(\n        DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);\n  }\n\n  /**\n   * Releases an offline license.\n   *\n   * @param offlineLicenseKeySetId The key set id of the license to be released.\n   * @throws DrmSessionException Thrown when a DRM session error occurs.\n   */\n  public synchronized void releaseLicense(byte[] offlineLicenseKeySetId)\n      throws DrmSessionException {\n    Assertions.checkNotNull(offlineLicenseKeySetId);\n    blockingKeyRequest(\n        DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);\n  }\n\n  /**\n   * Returns the remaining license and playback durations in seconds, for an offline license.\n   *\n   * @param offlineLicenseKeySetId The key set id of the license.\n   * @return The remaining license and playback durations, in seconds.\n   * @throws DrmSessionException Thrown when a DRM session error occurs.\n   */\n  public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)\n      throws DrmSessionException {\n    Assertions.checkNotNull(offlineLicenseKeySetId);\n    drmSessionManager.prepare();\n    DrmSession<T> drmSession =\n        openBlockingKeyRequest(\n            DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);\n    DrmSessionException error = drmSession.getError();\n    Pair<Long, Long> licenseDurationRemainingSec =\n        WidevineUtil.getLicenseDurationRemainingSec(drmSession);\n    drmSession.release();\n    drmSessionManager.release();\n    if (error != null) {\n      if (error.getCause() instanceof KeysExpiredException) {\n        return Pair.create(0L, 0L);\n      }\n      throw error;\n    }\n    return Assertions.checkNotNull(licenseDurationRemainingSec);\n  }\n\n  /**\n   * Releases the helper. Should be called when the helper is no longer required.\n   */\n  public void release() {\n    handlerThread.quit();\n  }\n\n  private byte[] blockingKeyRequest(\n      @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData)\n      throws DrmSessionException {\n    drmSessionManager.prepare();\n    DrmSession<T> drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,\n        drmInitData);\n    DrmSessionException error = drmSession.getError();\n    byte[] keySetId = drmSession.getOfflineLicenseKeySetId();\n    drmSession.release();\n    drmSessionManager.release();\n    if (error != null) {\n      throw error;\n    }\n    return Assertions.checkNotNull(keySetId);\n  }\n\n  private DrmSession<T> openBlockingKeyRequest(\n      @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) {\n    drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);\n    conditionVariable.close();\n    DrmSession<T> drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(),\n        drmInitData);\n    // Block current thread until key loading is finished\n    conditionVariable.block();\n    return drmSession;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport androidx.annotation.IntDef;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Thrown when the requested DRM scheme is not supported.\n */\npublic final class UnsupportedDrmException extends Exception {\n\n  /**\n   * The reason for the exception. One of {@link #REASON_UNSUPPORTED_SCHEME} or {@link\n   * #REASON_INSTANTIATION_ERROR}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR})\n  public @interface Reason {}\n  /**\n   * The requested DRM scheme is unsupported by the device.\n   */\n  public static final int REASON_UNSUPPORTED_SCHEME = 1;\n  /**\n   * There device advertises support for the requested DRM scheme, but there was an error\n   * instantiating it. The cause can be retrieved using {@link #getCause()}.\n   */\n  public static final int REASON_INSTANTIATION_ERROR = 2;\n\n  /**\n   * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.\n   */\n  @Reason public final int reason;\n\n  /**\n   * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.\n   */\n  public UnsupportedDrmException(@Reason int reason) {\n    this.reason = reason;\n  }\n\n  /**\n   * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.\n   * @param cause The cause of this exception.\n   */\n  public UnsupportedDrmException(@Reason int reason, Exception cause) {\n    super(cause);\n    this.reason = reason;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.drm;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.util.Map;\n\n/**\n * Utility methods for Widevine.\n */\npublic final class WidevineUtil {\n\n  /** Widevine specific key status field name for the remaining license duration, in seconds. */\n  public static final String PROPERTY_LICENSE_DURATION_REMAINING = \"LicenseDurationRemaining\";\n  /** Widevine specific key status field name for the remaining playback duration, in seconds. */\n  public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = \"PlaybackDurationRemaining\";\n\n  private WidevineUtil() {}\n\n  /**\n   * Returns license and playback durations remaining in seconds.\n   *\n   * @param drmSession The drm session to query.\n   * @return A {@link Pair} consisting of the remaining license and playback durations in seconds,\n   *     or null if called before the session has been opened or after it's been released.\n   */\n  public static @Nullable Pair<Long, Long> getLicenseDurationRemainingSec(\n      DrmSession<?> drmSession) {\n    Map<String, String> keyStatus = drmSession.queryKeyStatus();\n    if (keyStatus == null) {\n      return null;\n    }\n    return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),\n        getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));\n  }\n\n  private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {\n    if (keyStatus != null) {\n      try {\n        String value = keyStatus.get(property);\n        if (value != null) {\n          return Long.parseLong(value);\n        }\n      } catch (NumberFormatException e) {\n        // do nothing.\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/drm/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.drm;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\n\n/**\n * A seeker that supports seeking within a stream by searching for the target frame using binary\n * search.\n *\n * <p>This seeker operates on a stream that contains multiple frames (or samples). Each frame is\n * associated with some kind of timestamps, such as stream time, or frame indices. Given a target\n * seek time, the seeker will find the corresponding target timestamp, and perform a search\n * operation within the stream to identify the target frame and return the byte position in the\n * stream of the target frame.\n */\npublic abstract class BinarySearchSeeker {\n\n  /** A seeker that looks for a given timestamp from an input. */\n  protected interface TimestampSeeker {\n\n    /**\n     * Searches a limited window of the provided input for a target timestamp. The size of the\n     * window is implementation specific, but should be small enough such that it's reasonable for\n     * multiple such reads to occur during a seek operation.\n     *\n     * @param input The {@link ExtractorInput} from which data should be peeked.\n     * @param targetTimestamp The target timestamp.\n     * @param outputFrameHolder If {@link TimestampSearchResult#TYPE_TARGET_TIMESTAMP_FOUND} is\n     *     returned, this holder may be updated to hold the extracted frame that contains the target\n     *     frame/sample associated with the target timestamp.\n     * @return A {@link TimestampSearchResult} that describes the result of the search.\n     * @throws IOException If an error occurred reading from the input.\n     * @throws InterruptedException If the thread was interrupted.\n     */\n    TimestampSearchResult searchForTimestamp(\n            ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)\n        throws IOException, InterruptedException;\n\n    /** Called when a seek operation finishes. */\n    default void onSeekFinished() {}\n  }\n\n  /**\n   * Holds a frame extracted from a stream, together with the time stamp of the frame in\n   * microseconds.\n   */\n  public static final class OutputFrameHolder {\n\n    public final ByteBuffer byteBuffer;\n\n    public long timeUs;\n\n    /** Constructs an instance, wrapping the given byte buffer. */\n    public OutputFrameHolder(ByteBuffer outputByteBuffer) {\n      this.timeUs = 0;\n      this.byteBuffer = outputByteBuffer;\n    }\n  }\n\n  /**\n   * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the\n   * timestamp for a seek time position.\n   */\n  public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {\n\n    @Override\n    public long timeUsToTargetTime(long timeUs) {\n      return timeUs;\n    }\n  }\n\n  /**\n   * A converter that converts seek time in stream time into target timestamp for the {@link\n   * BinarySearchSeeker}.\n   */\n  protected interface SeekTimestampConverter {\n    /**\n     * Converts a seek time in microseconds into target timestamp for the {@link\n     * BinarySearchSeeker}.\n     */\n    long timeUsToTargetTime(long timeUs);\n  }\n\n  /**\n   * When seeking within the source, if the offset is smaller than or equal to this value, the seek\n   * operation will be performed using a skip operation. Otherwise, the source will be reloaded at\n   * the new seek position.\n   */\n  private static final long MAX_SKIP_BYTES = 256 * 1024;\n\n  protected final BinarySearchSeekMap seekMap;\n  protected final TimestampSeeker timestampSeeker;\n  protected @Nullable SeekOperationParams seekOperationParams;\n\n  private final int minimumSearchRange;\n\n  /**\n   * Constructs an instance.\n   *\n   * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in\n   *     stream time into target timestamp.\n   * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps\n   *     within the stream.\n   * @param durationUs The duration of the stream in microseconds.\n   * @param floorTimePosition The minimum timestamp value (inclusive) in the stream.\n   * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream.\n   * @param floorBytePosition The starting position of the frame with minimum timestamp value\n   *     (inclusive) in the stream.\n   * @param ceilingBytePosition The position after the frame with maximum timestamp value in the\n   *     stream.\n   * @param approxBytesPerFrame Approximated bytes per frame.\n   * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If\n   *     the remaining search range is smaller than this value, the search will stop, and the seeker\n   *     will return the position at the floor of the range as the result.\n   */\n  @SuppressWarnings(\"initialization\")\n  protected BinarySearchSeeker(\n      SeekTimestampConverter seekTimestampConverter,\n      TimestampSeeker timestampSeeker,\n      long durationUs,\n      long floorTimePosition,\n      long ceilingTimePosition,\n      long floorBytePosition,\n      long ceilingBytePosition,\n      long approxBytesPerFrame,\n      int minimumSearchRange) {\n    this.timestampSeeker = timestampSeeker;\n    this.minimumSearchRange = minimumSearchRange;\n    this.seekMap =\n        new BinarySearchSeekMap(\n            seekTimestampConverter,\n            durationUs,\n            floorTimePosition,\n            ceilingTimePosition,\n            floorBytePosition,\n            ceilingBytePosition,\n            approxBytesPerFrame);\n  }\n\n  /** Returns the seek map for the stream. */\n  public final SeekMap getSeekMap() {\n    return seekMap;\n  }\n\n  /**\n   * Sets the target time in microseconds within the stream to seek to.\n   *\n   * @param timeUs The target time in microseconds within the stream.\n   */\n  public final void setSeekTargetUs(long timeUs) {\n    if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) {\n      return;\n    }\n    seekOperationParams = createSeekParamsForTargetTimeUs(timeUs);\n  }\n\n  /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */\n  public final boolean isSeeking() {\n    return seekOperationParams != null;\n  }\n\n  /**\n   * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from\n   * {@link Extractor}.\n   *\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated\n   *     to hold the position of the required seek.\n   * @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be\n   *     updated to hold the extracted frame that contains the target sample. The caller needs to\n   *     check the byte buffer limit to see if an extracted frame is available.\n   * @return One of the {@code RESULT_} values defined in {@link Extractor}.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public int handlePendingSeek(\n      ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder)\n      throws InterruptedException, IOException {\n    TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);\n    while (true) {\n      SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams);\n      long floorPosition = seekOperationParams.getFloorBytePosition();\n      long ceilingPosition = seekOperationParams.getCeilingBytePosition();\n      long searchPosition = seekOperationParams.getNextSearchBytePosition();\n\n      if (ceilingPosition - floorPosition <= minimumSearchRange) {\n        // The seeking range is too small, so we can just continue from the floor position.\n        markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition);\n        return seekToPosition(input, floorPosition, seekPositionHolder);\n      }\n      if (!skipInputUntilPosition(input, searchPosition)) {\n        return seekToPosition(input, searchPosition, seekPositionHolder);\n      }\n\n      input.resetPeekPosition();\n      TimestampSearchResult timestampSearchResult =\n          timestampSeeker.searchForTimestamp(\n              input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);\n\n      switch (timestampSearchResult.type) {\n        case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:\n          seekOperationParams.updateSeekCeiling(\n              timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);\n          break;\n        case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:\n          seekOperationParams.updateSeekFloor(\n              timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);\n          break;\n        case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:\n          markSeekOperationFinished(\n              /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);\n          skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);\n          return seekToPosition(\n              input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);\n        case TimestampSearchResult.TYPE_NO_TIMESTAMP:\n          // We can't find any timestamp in the search range from the search position.\n          // Give up, and just continue reading from the last search position in this case.\n          markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);\n          return seekToPosition(input, searchPosition, seekPositionHolder);\n        default:\n          throw new IllegalStateException(\"Invalid case\");\n      }\n    }\n  }\n\n  protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) {\n    return new SeekOperationParams(\n        timeUs,\n        seekMap.timeUsToTargetTime(timeUs),\n        seekMap.floorTimePosition,\n        seekMap.ceilingTimePosition,\n        seekMap.floorBytePosition,\n        seekMap.ceilingBytePosition,\n        seekMap.approxBytesPerFrame);\n  }\n\n  protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {\n    seekOperationParams = null;\n    timestampSeeker.onSeekFinished();\n    onSeekOperationFinished(foundTargetFrame, resultPosition);\n  }\n\n  protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {\n    // Do nothing.\n  }\n\n  protected final boolean skipInputUntilPosition(ExtractorInput input, long position)\n      throws IOException, InterruptedException {\n    long bytesToSkip = position - input.getPosition();\n    if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {\n      input.skipFully((int) bytesToSkip);\n      return true;\n    }\n    return false;\n  }\n\n  protected final int seekToPosition(\n      ExtractorInput input, long position, PositionHolder seekPositionHolder) {\n    if (position == input.getPosition()) {\n      return Extractor.RESULT_CONTINUE;\n    } else {\n      seekPositionHolder.position = position;\n      return Extractor.RESULT_SEEK;\n    }\n  }\n\n  /**\n   * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}.\n   *\n   * <p>This class holds parameters for a binary-search for the {@code targetTimePosition} in the\n   * range [floorPosition, ceilingPosition).\n   */\n  protected static class SeekOperationParams {\n    private final long seekTimeUs;\n    private final long targetTimePosition;\n    private final long approxBytesPerFrame;\n\n    private long floorTimePosition;\n    private long ceilingTimePosition;\n    private long floorBytePosition;\n    private long ceilingBytePosition;\n    private long nextSearchBytePosition;\n\n    /**\n     * Returns the next position in the stream to search for target frame, given [floorBytePosition,\n     * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition).\n     */\n    protected static long calculateNextSearchBytePosition(\n        long targetTimePosition,\n        long floorTimePosition,\n        long ceilingTimePosition,\n        long floorBytePosition,\n        long ceilingBytePosition,\n        long approxBytesPerFrame) {\n      if (floorBytePosition + 1 >= ceilingBytePosition\n          || floorTimePosition + 1 >= ceilingTimePosition) {\n        return floorBytePosition;\n      }\n      long seekTimeDuration = targetTimePosition - floorTimePosition;\n      float estimatedBytesPerTimeUnit =\n          (float) (ceilingBytePosition - floorBytePosition)\n              / (ceilingTimePosition - floorTimePosition);\n      // It's better to under-estimate rather than over-estimate, because the extractor\n      // input can skip forward easily, but cannot rewind easily (it may require a new connection\n      // to be made).\n      // Therefore, we should reduce the estimated position by some amount, so it will converge to\n      // the correct frame earlier.\n      long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit);\n      long confidenceInterval = bytesToSkip / 20;\n      long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame;\n      long estimatedPosition = estimatedFramePosition - confidenceInterval;\n      return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1);\n    }\n\n    protected SeekOperationParams(\n        long seekTimeUs,\n        long targetTimePosition,\n        long floorTimePosition,\n        long ceilingTimePosition,\n        long floorBytePosition,\n        long ceilingBytePosition,\n        long approxBytesPerFrame) {\n      this.seekTimeUs = seekTimeUs;\n      this.targetTimePosition = targetTimePosition;\n      this.floorTimePosition = floorTimePosition;\n      this.ceilingTimePosition = ceilingTimePosition;\n      this.floorBytePosition = floorBytePosition;\n      this.ceilingBytePosition = ceilingBytePosition;\n      this.approxBytesPerFrame = approxBytesPerFrame;\n      this.nextSearchBytePosition =\n          calculateNextSearchBytePosition(\n              targetTimePosition,\n              floorTimePosition,\n              ceilingTimePosition,\n              floorBytePosition,\n              ceilingBytePosition,\n              approxBytesPerFrame);\n    }\n\n    /**\n     * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek\n     * operation.\n     */\n    private long getFloorBytePosition() {\n      return floorBytePosition;\n    }\n\n    /**\n     * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek\n     * operation.\n     */\n    private long getCeilingBytePosition() {\n      return ceilingBytePosition;\n    }\n\n    /** Returns the target timestamp as translated from the seek time. */\n    private long getTargetTimePosition() {\n      return targetTimePosition;\n    }\n\n    /** Returns the target seek time in microseconds. */\n    private long getSeekTimeUs() {\n      return seekTimeUs;\n    }\n\n    /** Updates the floor constraints (inclusive) of the seek operation. */\n    private void updateSeekFloor(long floorTimePosition, long floorBytePosition) {\n      this.floorTimePosition = floorTimePosition;\n      this.floorBytePosition = floorBytePosition;\n      updateNextSearchBytePosition();\n    }\n\n    /** Updates the ceiling constraints (exclusive) of the seek operation. */\n    private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) {\n      this.ceilingTimePosition = ceilingTimePosition;\n      this.ceilingBytePosition = ceilingBytePosition;\n      updateNextSearchBytePosition();\n    }\n\n    /** Returns the next position in the stream to search. */\n    private long getNextSearchBytePosition() {\n      return nextSearchBytePosition;\n    }\n\n    private void updateNextSearchBytePosition() {\n      this.nextSearchBytePosition =\n          calculateNextSearchBytePosition(\n              targetTimePosition,\n              floorTimePosition,\n              ceilingTimePosition,\n              floorBytePosition,\n              ceilingBytePosition,\n              approxBytesPerFrame);\n    }\n  }\n\n  /**\n   * Represents possible search results for {@link\n   * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}.\n   */\n  public static final class TimestampSearchResult {\n\n    /** The search found a timestamp that it deems close enough to the given target. */\n    public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0;\n    /** The search found only timestamps larger than the target timestamp. */\n    public static final int TYPE_POSITION_OVERESTIMATED = -1;\n    /** The search found only timestamps smaller than the target timestamp. */\n    public static final int TYPE_POSITION_UNDERESTIMATED = -2;\n    /** The search didn't find any timestamps. */\n    public static final int TYPE_NO_TIMESTAMP = -3;\n\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({\n      TYPE_TARGET_TIMESTAMP_FOUND,\n      TYPE_POSITION_OVERESTIMATED,\n      TYPE_POSITION_UNDERESTIMATED,\n      TYPE_NO_TIMESTAMP\n    })\n    @interface Type {}\n\n    public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =\n        new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);\n\n    /** The type of the result. */\n    @Type private final int type;\n\n    /**\n     * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link\n     * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link\n     * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link\n     * SeekOperationParams#floorTimePosition} should be updated with this value.\n     */\n    private final long timestampToUpdate;\n    /**\n     * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link\n     * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link\n     * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link\n     * SeekOperationParams#floorBytePosition} should be updated with this value.\n     */\n    private final long bytePositionToUpdate;\n\n    private TimestampSearchResult(\n        @Type int type, long timestampToUpdate, long bytePositionToUpdate) {\n      this.type = type;\n      this.timestampToUpdate = timestampToUpdate;\n      this.bytePositionToUpdate = bytePositionToUpdate;\n    }\n\n    /**\n     * Returns a result to signal that the current position in the input stream overestimates the\n     * true position of the target frame, and the {@link BinarySearchSeeker} should modify its\n     * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values.\n     */\n    public static TimestampSearchResult overestimatedResult(\n        long newCeilingTimestamp, long newCeilingBytePosition) {\n      return new TimestampSearchResult(\n          TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);\n    }\n\n    /**\n     * Returns a result to signal that the current position in the input stream underestimates the\n     * true position of the target frame, and the {@link BinarySearchSeeker} should modify its\n     * {@link SeekOperationParams}'s floor timestamp and byte position using the given values.\n     */\n    public static TimestampSearchResult underestimatedResult(\n        long newFloorTimestamp, long newCeilingBytePosition) {\n      return new TimestampSearchResult(\n          TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);\n    }\n\n    /**\n     * Returns a result to signal that the target timestamp has been found at {@code\n     * resultBytePosition}, and the seek operation can stop.\n     *\n     * <p>Note that when this value is returned from {@link\n     * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link\n     * OutputFrameHolder} may be updated to hold the target frame as an optimization.\n     */\n    public static TimestampSearchResult targetFoundResult(long resultBytePosition) {\n      return new TimestampSearchResult(\n          TYPE_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition);\n    }\n  }\n\n  /**\n   * A {@link SeekMap} implementation that returns the estimated byte location from {@link\n   * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for\n   * each {@link #getSeekPoints(long)} query.\n   */\n  public static class BinarySearchSeekMap implements SeekMap {\n    private final SeekTimestampConverter seekTimestampConverter;\n    private final long durationUs;\n    private final long floorTimePosition;\n    private final long ceilingTimePosition;\n    private final long floorBytePosition;\n    private final long ceilingBytePosition;\n    private final long approxBytesPerFrame;\n\n    /** Constructs a new instance of this seek map. */\n    public BinarySearchSeekMap(\n        SeekTimestampConverter seekTimestampConverter,\n        long durationUs,\n        long floorTimePosition,\n        long ceilingTimePosition,\n        long floorBytePosition,\n        long ceilingBytePosition,\n        long approxBytesPerFrame) {\n      this.seekTimestampConverter = seekTimestampConverter;\n      this.durationUs = durationUs;\n      this.floorTimePosition = floorTimePosition;\n      this.ceilingTimePosition = ceilingTimePosition;\n      this.floorBytePosition = floorBytePosition;\n      this.ceilingBytePosition = ceilingBytePosition;\n      this.approxBytesPerFrame = approxBytesPerFrame;\n    }\n\n    @Override\n    public boolean isSeekable() {\n      return true;\n    }\n\n    @Override\n    public SeekPoints getSeekPoints(long timeUs) {\n      long nextSearchPosition =\n          SeekOperationParams.calculateNextSearchBytePosition(\n              /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs),\n              /* floorTimePosition= */ floorTimePosition,\n              /* ceilingTimePosition= */ ceilingTimePosition,\n              /* floorBytePosition= */ floorBytePosition,\n              /* ceilingBytePosition= */ ceilingBytePosition,\n              /* approxBytesPerFrame= */ approxBytesPerFrame);\n      return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));\n    }\n\n    @Override\n    public long getDurationUs() {\n      return durationUs;\n    }\n\n    /** @see SeekTimestampConverter#timeUsToTargetTime(long) */\n    public long timeUsToTargetTime(long timeUs) {\n      return seekTimestampConverter.timeUsToTargetTime(timeUs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Defines chunks of samples within a media stream.\n */\npublic final class ChunkIndex implements SeekMap {\n\n  /**\n   * The number of chunks.\n   */\n  public final int length;\n\n  /**\n   * The chunk sizes, in bytes.\n   */\n  public final int[] sizes;\n\n  /**\n   * The chunk byte offsets.\n   */\n  public final long[] offsets;\n\n  /**\n   * The chunk durations, in microseconds.\n   */\n  public final long[] durationsUs;\n\n  /**\n   * The start time of each chunk, in microseconds.\n   */\n  public final long[] timesUs;\n\n  private final long durationUs;\n\n  /**\n   * @param sizes The chunk sizes, in bytes.\n   * @param offsets The chunk byte offsets.\n   * @param durationsUs The chunk durations, in microseconds.\n   * @param timesUs The start time of each chunk, in microseconds.\n   */\n  public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) {\n    this.sizes = sizes;\n    this.offsets = offsets;\n    this.durationsUs = durationsUs;\n    this.timesUs = timesUs;\n    length = sizes.length;\n    if (length > 0) {\n      durationUs = durationsUs[length - 1] + timesUs[length - 1];\n    } else {\n      durationUs = 0;\n    }\n  }\n\n  /**\n   * Obtains the index of the chunk corresponding to a given time.\n   *\n   * @param timeUs The time, in microseconds.\n   * @return The index of the corresponding chunk.\n   */\n  public int getChunkIndex(long timeUs) {\n    return Util.binarySearchFloor(timesUs, timeUs, true, true);\n  }\n\n  // SeekMap implementation.\n\n  @Override\n  public boolean isSeekable() {\n    return true;\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    int chunkIndex = getChunkIndex(timeUs);\n    SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]);\n    if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) {\n      return new SeekPoints(seekPoint);\n    } else {\n      SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]);\n      return new SeekPoints(seekPoint, nextSeekPoint);\n    }\n  }\n\n  @Override\n  public String toString() {\n    return \"ChunkIndex(\"\n        + \"length=\"\n        + length\n        + \", sizes=\"\n        + Arrays.toString(sizes)\n        + \", offsets=\"\n        + Arrays.toString(offsets)\n        + \", timeUs=\"\n        + Arrays.toString(timesUs)\n        + \", durationsUs=\"\n        + Arrays.toString(durationsUs)\n        + \")\";\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of\n * multiple independent frames of the same size. Seek points are calculated to be at frame\n * boundaries.\n */\npublic class ConstantBitrateSeekMap implements SeekMap {\n\n  private final long inputLength;\n  private final long firstFrameBytePosition;\n  private final int frameSize;\n  private final long dataSize;\n  private final int bitrate;\n  private final long durationUs;\n\n  /**\n   * Constructs a new instance from a stream.\n   *\n   * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.\n   * @param firstFrameBytePosition The byte-position of the first frame in the stream.\n   * @param bitrate The bitrate (which is assumed to be constant in the stream).\n   * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}\n   *     if unknown.\n   */\n  public ConstantBitrateSeekMap(\n      long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {\n    this.inputLength = inputLength;\n    this.firstFrameBytePosition = firstFrameBytePosition;\n    this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;\n    this.bitrate = bitrate;\n\n    if (inputLength == C.LENGTH_UNSET) {\n      dataSize = C.LENGTH_UNSET;\n      durationUs = C.TIME_UNSET;\n    } else {\n      dataSize = inputLength - firstFrameBytePosition;\n      durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate);\n    }\n  }\n\n  @Override\n  public boolean isSeekable() {\n    return dataSize != C.LENGTH_UNSET;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    if (dataSize == C.LENGTH_UNSET) {\n      return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));\n    }\n    long seekFramePosition = getFramePositionForTimeUs(timeUs);\n    long seekTimeUs = getTimeUsAtPosition(seekFramePosition);\n    SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);\n    if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) {\n      return new SeekPoints(seekPoint);\n    } else {\n      long secondSeekPosition = seekFramePosition + frameSize;\n      long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition);\n      SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);\n      return new SeekPoints(seekPoint, secondSeekPoint);\n    }\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  /**\n   * Returns the stream time in microseconds for a given position.\n   *\n   * @param position The stream byte-position.\n   * @return The stream time in microseconds for the given position.\n   */\n  public long getTimeUsAtPosition(long position) {\n    return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate);\n  }\n\n  // Internal methods\n\n  /**\n   * Returns the stream time in microseconds for a given stream position.\n   *\n   * @param position The stream byte-position.\n   * @param firstFrameBytePosition The position of the first frame in the stream.\n   * @param bitrate The bitrate (which is assumed to be constant in the stream).\n   * @return The stream time in microseconds for the given stream position.\n   */\n  private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) {\n    return Math.max(0, position - firstFrameBytePosition)\n        * C.BITS_PER_BYTE\n        * C.MICROS_PER_SECOND\n        / bitrate;\n  }\n\n  private long getFramePositionForTimeUs(long timeUs) {\n    long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);\n    // Constrain to nearest preceding frame offset.\n    positionOffset = (positionOffset / frameSize) * frameSize;\n    positionOffset =\n        Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize);\n    return firstFrameBytePosition + positionOffset;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Arrays;\n\n/**\n * An {@link ExtractorInput} that wraps a {@link DataSource}.\n */\npublic final class DefaultExtractorInput implements ExtractorInput {\n\n  private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;\n  private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;\n  private static final int SCRATCH_SPACE_SIZE = 4096;\n\n  private final byte[] scratchSpace;\n  private final DataSource dataSource;\n  private final long streamLength;\n\n  private long position;\n  private byte[] peekBuffer;\n  private int peekBufferPosition;\n  private int peekBufferLength;\n\n  /**\n   * @param dataSource The wrapped {@link DataSource}.\n   * @param position The initial position in the stream.\n   * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown.\n   */\n  public DefaultExtractorInput(DataSource dataSource, long position, long length) {\n    this.dataSource = dataSource;\n    this.position = position;\n    this.streamLength = length;\n    peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];\n    scratchSpace = new byte[SCRATCH_SPACE_SIZE];\n  }\n\n  @Override\n  public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {\n    int bytesRead = readFromPeekBuffer(target, offset, length);\n    if (bytesRead == 0) {\n      bytesRead = readFromDataSource(target, offset, length, 0, true);\n    }\n    commitBytesRead(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    int bytesRead = readFromPeekBuffer(target, offset, length);\n    while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) {\n      bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput);\n    }\n    commitBytesRead(bytesRead);\n    return bytesRead != C.RESULT_END_OF_INPUT;\n  }\n\n  @Override\n  public void readFully(byte[] target, int offset, int length)\n      throws IOException, InterruptedException {\n    readFully(target, offset, length, false);\n  }\n\n  @Override\n  public int skip(int length) throws IOException, InterruptedException {\n    int bytesSkipped = skipFromPeekBuffer(length);\n    if (bytesSkipped == 0) {\n      bytesSkipped =\n          readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true);\n    }\n    commitBytesRead(bytesSkipped);\n    return bytesSkipped;\n  }\n\n  @Override\n  public boolean skipFully(int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    int bytesSkipped = skipFromPeekBuffer(length);\n    while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {\n      int minLength = Math.min(length, bytesSkipped + scratchSpace.length);\n      bytesSkipped =\n          readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput);\n    }\n    commitBytesRead(bytesSkipped);\n    return bytesSkipped != C.RESULT_END_OF_INPUT;\n  }\n\n  @Override\n  public void skipFully(int length) throws IOException, InterruptedException {\n    skipFully(length, false);\n  }\n\n  @Override\n  public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    if (!advancePeekPosition(length, allowEndOfInput)) {\n      return false;\n    }\n    System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length);\n    return true;\n  }\n\n  @Override\n  public void peekFully(byte[] target, int offset, int length)\n      throws IOException, InterruptedException {\n    peekFully(target, offset, length, false);\n  }\n\n  @Override\n  public boolean advancePeekPosition(int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    ensureSpaceForPeek(length);\n    int bytesPeeked = peekBufferLength - peekBufferPosition;\n    while (bytesPeeked < length) {\n      bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked,\n          allowEndOfInput);\n      if (bytesPeeked == C.RESULT_END_OF_INPUT) {\n        return false;\n      }\n      peekBufferLength = peekBufferPosition + bytesPeeked;\n    }\n    peekBufferPosition += length;\n    return true;\n  }\n\n  @Override\n  public void advancePeekPosition(int length) throws IOException, InterruptedException {\n    advancePeekPosition(length, false);\n  }\n\n  @Override\n  public void resetPeekPosition() {\n    peekBufferPosition = 0;\n  }\n\n  @Override\n  public long getPeekPosition() {\n    return position + peekBufferPosition;\n  }\n\n  @Override\n  public long getPosition() {\n    return position;\n  }\n\n  @Override\n  public long getLength() {\n    return streamLength;\n  }\n\n  @Override\n  public <E extends Throwable> void setRetryPosition(long position, E e) throws E {\n    Assertions.checkArgument(position >= 0);\n    this.position = position;\n    throw e;\n  }\n\n  /**\n   * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the\n   * current peek position.\n   */\n  private void ensureSpaceForPeek(int length) {\n    int requiredLength = peekBufferPosition + length;\n    if (requiredLength > peekBuffer.length) {\n      int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,\n          requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);\n      peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);\n    }\n  }\n\n  /**\n   * Skips from the peek buffer.\n   *\n   * @param length The maximum number of bytes to skip from the peek buffer.\n   * @return The number of bytes skipped.\n   */\n  private int skipFromPeekBuffer(int length) {\n    int bytesSkipped = Math.min(peekBufferLength, length);\n    updatePeekBuffer(bytesSkipped);\n    return bytesSkipped;\n  }\n\n  /**\n   * Reads from the peek buffer\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The maximum number of bytes to read from the peek buffer.\n   * @return The number of bytes read.\n   */\n  private int readFromPeekBuffer(byte[] target, int offset, int length) {\n    if (peekBufferLength == 0) {\n      return 0;\n    }\n    int peekBytes = Math.min(peekBufferLength, length);\n    System.arraycopy(peekBuffer, 0, target, offset, peekBytes);\n    updatePeekBuffer(peekBytes);\n    return peekBytes;\n  }\n\n  /**\n   * Updates the peek buffer's length, position and contents after consuming data.\n   *\n   * @param bytesConsumed The number of bytes consumed from the peek buffer.\n   */\n  private void updatePeekBuffer(int bytesConsumed) {\n    peekBufferLength -= bytesConsumed;\n    peekBufferPosition = 0;\n    byte[] newPeekBuffer = peekBuffer;\n    if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {\n      newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];\n    }\n    System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);\n    peekBuffer = newPeekBuffer;\n  }\n\n  /**\n   * Starts or continues a read from the data source.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The maximum number of bytes to read from the input.\n   * @param bytesAlreadyRead The number of bytes already read from the input.\n   * @param allowEndOfInput True if encountering the end of the input having read no data is\n   *     allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it\n   *     should be considered an error, causing an {@link EOFException} to be thrown.\n   * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if\n   *     {@code allowEndOfInput} is true and the input has ended having read no bytes.\n   * @throws EOFException If the end of input was encountered having partially satisfied the read\n   *     (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were\n   *     read and {@code allowEndOfInput} is false.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead,\n      boolean allowEndOfInput) throws InterruptedException, IOException {\n    if (Thread.interrupted()) {\n      throw new InterruptedException();\n    }\n    int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead);\n    if (bytesRead == C.RESULT_END_OF_INPUT) {\n      if (bytesAlreadyRead == 0 && allowEndOfInput) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      throw new EOFException();\n    }\n    return bytesAlreadyRead + bytesRead;\n  }\n\n  /**\n   * Advances the position by the specified number of bytes read.\n   *\n   * @param bytesRead The number of bytes read.\n   */\n  private void commitBytesRead(int bytesRead) {\n    if (bytesRead != C.RESULT_END_OF_INPUT) {\n      position += bytesRead;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.extractor.amr.AmrExtractor;\nimport com.google.android.exoplayer2.extractor.flv.FlvExtractor;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\nimport com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;\nimport com.google.android.exoplayer2.extractor.ogg.OggExtractor;\nimport com.google.android.exoplayer2.extractor.ts.Ac3Extractor;\nimport com.google.android.exoplayer2.extractor.ts.Ac4Extractor;\nimport com.google.android.exoplayer2.extractor.ts.AdtsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;\nimport com.google.android.exoplayer2.extractor.ts.PsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.TsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader;\nimport com.google.android.exoplayer2.extractor.wav.WavExtractor;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.lang.reflect.Constructor;\n\n/**\n * An {@link ExtractorsFactory} that provides an array of extractors for the following formats:\n *\n * <ul>\n *   <li>MP4, including M4A ({@link Mp4Extractor})\n *   <li>fMP4 ({@link FragmentedMp4Extractor})\n *   <li>Matroska and WebM ({@link MatroskaExtractor})\n *   <li>Ogg Vorbis/FLAC ({@link OggExtractor}\n *   <li>MP3 ({@link Mp3Extractor})\n *   <li>AAC ({@link AdtsExtractor})\n *   <li>MPEG TS ({@link TsExtractor})\n *   <li>MPEG PS ({@link PsExtractor})\n *   <li>FLV ({@link FlvExtractor})\n *   <li>WAV ({@link WavExtractor})\n *   <li>AC3 ({@link Ac3Extractor})\n *   <li>AC4 ({@link Ac4Extractor})\n *   <li>AMR ({@link AmrExtractor})\n *   <li>FLAC (only available if the FLAC extension is built and included)\n * </ul>\n */\npublic final class DefaultExtractorsFactory implements ExtractorsFactory {\n\n  private static final Constructor<? extends Extractor> FLAC_EXTRACTOR_CONSTRUCTOR;\n  static {\n    Constructor<? extends Extractor> flacExtractorConstructor = null;\n    try {\n      // LINT.IfChange\n      flacExtractorConstructor =\n          Class.forName(\"com.google.android.exoplayer2.ext.flac.FlacExtractor\")\n              .asSubclass(Extractor.class)\n              .getConstructor();\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the FLAC extension.\n    } catch (Exception e) {\n      // The FLAC extension is present, but instantiation failed.\n      throw new RuntimeException(\"Error instantiating FLAC extension\", e);\n    }\n    FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;\n  }\n\n  private boolean constantBitrateSeekingEnabled;\n  private @AdtsExtractor.Flags int adtsFlags;\n  private @AmrExtractor.Flags int amrFlags;\n  private @MatroskaExtractor.Flags int matroskaFlags;\n  private @Mp4Extractor.Flags int mp4Flags;\n  private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;\n  private @Mp3Extractor.Flags int mp3Flags;\n  private @TsExtractor.Mode int tsMode;\n  private @DefaultTsPayloadReaderFactory.Flags int tsFlags;\n\n  public DefaultExtractorsFactory() {\n    tsMode = TsExtractor.MODE_SINGLE_PMT;\n  }\n\n  /**\n   * Convenience method to set whether approximate seeking using constant bitrate assumptions should\n   * be enabled for all extractors that support it. If set to true, the flags required to enable\n   * this functionality will be OR'd with those passed to the setters when creating extractor\n   * instances. If set to false then the flags passed to the setters will be used without\n   * modification.\n   *\n   * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate\n   *     assumption should be enabled for all extractors that support it.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setConstantBitrateSeekingEnabled(\n      boolean constantBitrateSeekingEnabled) {\n    this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link AdtsExtractor} instances created by the factory.\n   *\n   * @see AdtsExtractor#AdtsExtractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setAdtsExtractorFlags(\n      @AdtsExtractor.Flags int flags) {\n    this.adtsFlags = flags;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link AmrExtractor} instances created by the factory.\n   *\n   * @see AmrExtractor#AmrExtractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) {\n    this.amrFlags = flags;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link MatroskaExtractor} instances created by the factory.\n   *\n   * @see MatroskaExtractor#MatroskaExtractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags(\n      @MatroskaExtractor.Flags int flags) {\n    this.matroskaFlags = flags;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link Mp4Extractor} instances created by the factory.\n   *\n   * @see Mp4Extractor#Mp4Extractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) {\n    this.mp4Flags = flags;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory.\n   *\n   * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags(\n      @FragmentedMp4Extractor.Flags int flags) {\n    this.fragmentedMp4Flags = flags;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link Mp3Extractor} instances created by the factory.\n   *\n   * @see Mp3Extractor#Mp3Extractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) {\n    mp3Flags = flags;\n    return this;\n  }\n\n  /**\n   * Sets the mode for {@link TsExtractor} instances created by the factory.\n   *\n   * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory)\n   * @param mode The mode to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) {\n    tsMode = mode;\n    return this;\n  }\n\n  /**\n   * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances\n   * created by the factory.\n   *\n   * @see TsExtractor#TsExtractor(int)\n   * @param flags The flags to use.\n   * @return The factory, for convenience.\n   */\n  public synchronized DefaultExtractorsFactory setTsExtractorFlags(\n      @DefaultTsPayloadReaderFactory.Flags int flags) {\n    tsFlags = flags;\n    return this;\n  }\n\n  @Override\n  public synchronized Extractor[] createExtractors() {\n    Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14];\n    extractors[0] = new MatroskaExtractor(matroskaFlags);\n    extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);\n    extractors[2] = new Mp4Extractor(mp4Flags);\n    extractors[3] =\n        new Mp3Extractor(\n            mp3Flags\n                | (constantBitrateSeekingEnabled\n                    ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING\n                    : 0));\n    extractors[4] =\n        new AdtsExtractor(\n            adtsFlags\n                | (constantBitrateSeekingEnabled\n                    ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING\n                    : 0));\n    extractors[5] = new Ac3Extractor();\n    extractors[6] = new TsExtractor(tsMode, tsFlags);\n    extractors[7] = new FlvExtractor();\n    extractors[8] = new OggExtractor();\n    extractors[9] = new PsExtractor();\n    extractors[10] = new WavExtractor();\n    extractors[11] =\n        new AmrExtractor(\n            amrFlags\n                | (constantBitrateSeekingEnabled\n                    ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING\n                    : 0));\n    extractors[12] = new Ac4Extractor();\n    if (FLAC_EXTRACTOR_CONSTRUCTOR != null) {\n      try {\n        extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();\n      } catch (Exception e) {\n        // Should never happen.\n        throw new IllegalStateException(\"Unexpected error creating FLAC extractor\", e);\n      }\n    }\n    return extractors;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\n/** A dummy {@link ExtractorOutput} implementation. */\npublic final class DummyExtractorOutput implements ExtractorOutput {\n\n  @Override\n  public TrackOutput track(int id, int type) {\n    return new DummyTrackOutput();\n  }\n\n  @Override\n  public void endTracks() {\n    // Do nothing.\n  }\n\n  @Override\n  public void seekMap(SeekMap seekMap) {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/**\n * A dummy {@link TrackOutput} implementation.\n */\npublic final class DummyTrackOutput implements TrackOutput {\n\n  @Override\n  public void format(Format format) {\n    // Do nothing.\n  }\n\n  @Override\n  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    int bytesSkipped = input.skip(length);\n    if (bytesSkipped == C.RESULT_END_OF_INPUT) {\n      if (allowEndOfInput) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      throw new EOFException();\n    }\n    return bytesSkipped;\n  }\n\n  @Override\n  public void sampleData(ParsableByteArray data, int length) {\n    data.skipBytes(length);\n  }\n\n  @Override\n  public void sampleMetadata(\n      long timeUs,\n      @C.BufferFlags int flags,\n      int size,\n      int offset,\n      @Nullable CryptoData cryptoData) {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Extracts media data from a container format.\n */\npublic interface Extractor {\n\n  /**\n   * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed\n   * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data\n   * continuing from the position in the stream reached by the returning call.\n   */\n  int RESULT_CONTINUE = 0;\n  /**\n   * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed\n   * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting\n   * from a specified position in the stream.\n   */\n  int RESULT_SEEK = 1;\n  /**\n   * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the\n   * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}.\n   */\n  int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;\n\n  /**\n   * Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. One of\n   * {@link #RESULT_CONTINUE}, {@link #RESULT_SEEK} or {@link #RESULT_END_OF_INPUT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT})\n  @interface ReadResult {}\n\n  /**\n   * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must\n   * provide data from the start of the stream.\n   * <p>\n   * If {@code true} is returned, the {@code input}'s reading position may have been modified.\n   * Otherwise, only its peek position may have been modified.\n   *\n   * @param input The {@link ExtractorInput} from which data should be peeked/read.\n   * @return Whether this extractor can read the provided input.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  boolean sniff(ExtractorInput input) throws IOException, InterruptedException;\n\n  /**\n   * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.\n   *\n   * @param output An {@link ExtractorOutput} to receive extracted data.\n   */\n  void init(ExtractorOutput output);\n\n  /**\n   * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link\n   * #init(ExtractorOutput)}.\n   *\n   * <p>A single call to this method will block until some progress has been made, but will not\n   * block for longer than this. Hence each call will consume only a small amount of input data.\n   *\n   * <p>In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link\n   * ExtractorInput} passed to the next read is required to provide data continuing from the\n   * position in the stream reached by the returning call. If the extractor requires data to be\n   * provided from a different position, then that position is set in {@code seekPosition} and\n   * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the\n   * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned.\n   *\n   * <p>When this method throws an {@link IOException} or an {@link InterruptedException},\n   * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link\n   * ExtractorInput#getPosition() read position} to a subsequent call to this method.\n   *\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the\n   *     position of the required data.\n   * @return One of the {@code RESULT_} values defined in this interface.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  @ReadResult\n  int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException;\n\n  /**\n   * Notifies the extractor that a seek has occurred.\n   * <p>\n   * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of\n   * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from {@code\n   * position} in the stream. Valid random access positions are the start of the stream and\n   * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}.\n   *\n   * @param position The byte offset in the stream from which data will be provided.\n   * @param timeUs The seek time in microseconds.\n   */\n  void seek(long position, long timeUs);\n\n  /**\n   * Releases all kept resources.\n   */\n  void release();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.C;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Provides data to be consumed by an {@link Extractor}.\n *\n * <p>This interface provides two modes of accessing the underlying input. See the subheadings below\n * for more info about each mode.\n *\n * <ul>\n *   <li>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level\n *       access operations.\n *   <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user\n *       wants to read an entire block/frame/header of known length.\n * </ul>\n *\n * <h3>{@link InputStream}-like methods</h3>\n *\n * <p>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level\n * access operations. The {@code length} parameter is a maximum, and each method returns the number\n * of bytes actually processed. This may be less than {@code length} because the end of the input\n * was reached, or the method was interrupted, or the operation was aborted early for another\n * reason.\n *\n * <h3>Block-based methods</h3>\n *\n * <p>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user\n * wants to read an entire block/frame/header of known length.\n *\n * <p>These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This\n * parameter is intended to be set to true when the caller believes the input might be fully\n * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final\n * block/frame/header). It's <b>not</b> intended to allow a partial read (i.e. greater than 0 bytes,\n * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from\n * these methods (a partial read is assumed to indicate a malformed block/frame/header - and\n * therefore a malformed file).\n *\n * <p>The expected behaviour of the block-based methods is therefore:\n *\n * <ul>\n *   <li>Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}.\n *   <li>Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}.\n *   <li>Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException}\n *       (regardless of {@code allowEndOfInput}).\n * </ul>\n */\npublic interface ExtractorInput {\n\n  /**\n   * Reads up to {@code length} bytes from the input and resets the peek position.\n   * <p>\n   * This method blocks until at least one byte of data can be read, the end of the input is\n   * detected, or an exception is thrown.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The maximum number of bytes to read from the input.\n   * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  int read(byte[] target, int offset, int length) throws IOException, InterruptedException;\n\n  /**\n   * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The number of bytes to read from the input.\n   * @param allowEndOfInput True if encountering the end of the input having read no data is\n   *     allowed, and should result in {@code false} being returned. False if it should be\n   *     considered an error, causing an {@link EOFException} to be thrown. See note in class\n   *     Javadoc.\n   * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of\n   *     the input was encountered having read no data.\n   * @throws EOFException If the end of input was encountered having partially satisfied the read\n   *     (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were\n   *     read and {@code allowEndOfInput} is false.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException;\n\n  /**\n   * Equivalent to {@code readFully(target, offset, length, false)}.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The number of bytes to read from the input.\n   * @throws EOFException If the end of input was encountered.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;\n\n  /**\n   * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read.\n   *\n   * @param length The maximum number of bytes to skip from the input.\n   * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  int skip(int length) throws IOException, InterruptedException;\n\n  /**\n   * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read.\n   *\n   * @param length The number of bytes to skip from the input.\n   * @param allowEndOfInput True if encountering the end of the input having skipped no data is\n   *     allowed, and should result in {@code false} being returned. False if it should be\n   *     considered an error, causing an {@link EOFException} to be thrown. See note in class\n   *     Javadoc.\n   * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of\n   *     the input was encountered having skipped no data.\n   * @throws EOFException If the end of input was encountered having partially satisfied the skip\n   *     (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were\n   *     skipped and {@code allowEndOfInput} is false.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException;\n\n  /**\n   * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.\n   * <p>\n   * Encountering the end of input is always considered an error, and will result in an\n   * {@link EOFException} being thrown.\n   *\n   * @param length The number of bytes to skip from the input.\n   * @throws EOFException If the end of input was encountered.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  void skipFully(int length) throws IOException, InterruptedException;\n\n  /**\n   * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index\n   * {@code offset}. The current read position is left unchanged.\n   *\n   * <p>Calling {@link #resetPeekPosition()} resets the peek position to equal the current read\n   * position, so the caller can peek the same data again. Reading or skipping also resets the peek\n   * position.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The number of bytes to peek from the input.\n   * @param allowEndOfInput True if encountering the end of the input having peeked no data is\n   *     allowed, and should result in {@code false} being returned. False if it should be\n   *     considered an error, causing an {@link EOFException} to be thrown. See note in class\n   *     Javadoc.\n   * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of\n   *     the input was encountered having peeked no data.\n   * @throws EOFException If the end of input was encountered having partially satisfied the peek\n   *     (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were\n   *     peeked and {@code allowEndOfInput} is false.\n   * @throws IOException If an error occurs peeking from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException;\n\n  /**\n   * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index\n   * {@code offset}. The current read position is left unchanged.\n   * <p>\n   * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read\n   * position, so the caller can peek the same data again. Reading and skipping also reset the peek\n   * position.\n   *\n   * @param target A target array into which data should be written.\n   * @param offset The offset into the target array at which to write.\n   * @param length The number of bytes to peek from the input.\n   * @throws EOFException If the end of input was encountered.\n   * @throws IOException If an error occurs peeking from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException;\n\n  /**\n   * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,\n   * boolean)} except the data is skipped instead of read.\n   *\n   * @param length The number of bytes by which to advance the peek position.\n   * @param allowEndOfInput True if encountering the end of the input before advancing is allowed,\n   *     and should result in {@code false} being returned. False if it should be considered an\n   *     error, causing an {@link EOFException} to be thrown. See note in class Javadoc.\n   * @return True if advancing the peek position was successful. False if {@code\n   *     allowEndOfInput=true} and the end of the input was encountered before advancing over any\n   *     data.\n   * @throws EOFException If the end of input was encountered having partially advanced (i.e. having\n   *     advanced by at least one byte, but fewer than {@code length}), or if the end of input was\n   *     encountered before advancing and {@code allowEndOfInput} is false.\n   * @throws IOException If an error occurs advancing the peek position.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  boolean advancePeekPosition(int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException;\n\n  /**\n   * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)}\n   * except the data is skipped instead of read.\n   *\n   * @param length The number of bytes to peek from the input.\n   * @throws EOFException If the end of input was encountered.\n   * @throws IOException If an error occurs peeking from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  void advancePeekPosition(int length) throws IOException, InterruptedException;\n\n  /**\n   * Resets the peek position to equal the current read position.\n   */\n  void resetPeekPosition();\n\n  /**\n   * Returns the current peek position (byte offset) in the stream.\n   *\n   * @return The peek position (byte offset) in the stream.\n   */\n  long getPeekPosition();\n\n  /**\n   * Returns the current read position (byte offset) in the stream.\n   *\n   * @return The read position (byte offset) in the stream.\n   */\n  long getPosition();\n\n  /**\n   * Returns the length of the source stream, or {@link C#LENGTH_UNSET} if it is unknown.\n   *\n   * @return The length of the source stream, or {@link C#LENGTH_UNSET}.\n   */\n  long getLength();\n\n  /**\n   * Called when reading fails and the required retry position is different from the last position.\n   * After setting the retry position it throws the given {@link Throwable}.\n   *\n   * @param <E> Type of {@link Throwable} to be thrown.\n   * @param position The required retry position.\n   * @param e {@link Throwable} to be thrown.\n   * @throws E The given {@link Throwable} object.\n   */\n  <E extends Throwable> void setRetryPosition(long position, E e) throws E;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\n/**\n * Receives stream level data extracted by an {@link Extractor}.\n */\npublic interface ExtractorOutput {\n\n  /**\n   * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.\n   * <p>\n   * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.\n   *\n   * @param id A track identifier.\n   * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}\n   *     {@code TRACK_TYPE_*} constants.\n   * @return The {@link TrackOutput} for the given track identifier.\n   */\n  TrackOutput track(int id, int type);\n\n  /**\n   * Called when all tracks have been identified, meaning no new {@code trackId} values will be\n   * passed to {@link #track(int, int)}.\n   */\n  void endTracks();\n\n  /**\n   * Called when a {@link SeekMap} has been extracted from the stream.\n   *\n   * @param seekMap The extracted {@link SeekMap}.\n   */\n  void seekMap(SeekMap seekMap);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\n/** Factory for arrays of {@link Extractor} instances. */\npublic interface ExtractorsFactory {\n\n  /** Returns an array of new {@link Extractor} instances. */\n  Extractor[] createExtractors();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.CommentFrame;\nimport com.google.android.exoplayer2.metadata.id3.InternalFrame;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Holder for gapless playback information.\n */\npublic final class GaplessInfoHolder {\n\n  private static final String GAPLESS_DOMAIN = \"com.apple.iTunes\";\n  private static final String GAPLESS_DESCRIPTION = \"iTunSMPB\";\n  private static final Pattern GAPLESS_COMMENT_PATTERN =\n      Pattern.compile(\"^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})\");\n\n  /**\n   * The number of samples to trim from the start of the decoded audio stream, or\n   * {@link Format#NO_VALUE} if not set.\n   */\n  public int encoderDelay;\n\n  /**\n   * The number of samples to trim from the end of the decoded audio stream, or\n   * {@link Format#NO_VALUE} if not set.\n   */\n  public int encoderPadding;\n\n  /**\n   * Creates a new holder for gapless playback information.\n   */\n  public GaplessInfoHolder() {\n    encoderDelay = Format.NO_VALUE;\n    encoderPadding = Format.NO_VALUE;\n  }\n\n  /**\n   * Populates the holder with data from an MP3 Xing header, if valid and non-zero.\n   *\n   * @param value The 24-bit value to decode.\n   * @return Whether the holder was populated.\n   */\n  public boolean setFromXingHeaderValue(int value) {\n    int encoderDelay = value >> 12;\n    int encoderPadding = value & 0x0FFF;\n    if (encoderDelay > 0 || encoderPadding > 0) {\n      this.encoderDelay = encoderDelay;\n      this.encoderPadding = encoderPadding;\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * Populates the holder with data parsed from ID3 {@link Metadata}.\n   *\n   * @param metadata The metadata from which to parse the gapless information.\n   * @return Whether the holder was populated.\n   */\n  public boolean setFromMetadata(Metadata metadata) {\n    for (int i = 0; i < metadata.length(); i++) {\n      Metadata.Entry entry = metadata.get(i);\n      if (entry instanceof CommentFrame) {\n        CommentFrame commentFrame = (CommentFrame) entry;\n        if (GAPLESS_DESCRIPTION.equals(commentFrame.description)\n            && setFromComment(commentFrame.text)) {\n          return true;\n        }\n      } else if (entry instanceof InternalFrame) {\n        InternalFrame internalFrame = (InternalFrame) entry;\n        if (GAPLESS_DOMAIN.equals(internalFrame.domain)\n            && GAPLESS_DESCRIPTION.equals(internalFrame.description)\n            && setFromComment(internalFrame.text)) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header\n   * or MPEG 4 user data), if valid and non-zero.\n   *\n   * @param data The comment's payload data.\n   * @return Whether the holder was populated.\n   */\n  private boolean setFromComment(String data) {\n    Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);\n    if (matcher.find()) {\n      try {\n        int encoderDelay = Integer.parseInt(matcher.group(1), 16);\n        int encoderPadding = Integer.parseInt(matcher.group(2), 16);\n        if (encoderDelay > 0 || encoderPadding > 0) {\n          this.encoderDelay = encoderDelay;\n          this.encoderPadding = encoderPadding;\n          return true;\n        }\n      } catch (NumberFormatException e) {\n        // Ignore incorrectly formatted comments.\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.\n   */\n  public boolean hasGaplessInfo() {\n    return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/**\n * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag.\n */\npublic final class Id3Peeker {\n\n  private final ParsableByteArray scratch;\n\n  public Id3Peeker() {\n    scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);\n  }\n\n  /**\n   * Peeks ID3 data from the input and parses the first ID3 tag.\n   *\n   * @param input The {@link ExtractorInput} from which data should be peeked.\n   * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all\n   *     frames.\n   * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not\n   *     present in the input.\n   * @throws IOException If an error occurred peeking from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  @Nullable\n  public Metadata peekId3Data(\n      ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate)\n      throws IOException, InterruptedException {\n    int peekedId3Bytes = 0;\n    Metadata metadata = null;\n    while (true) {\n      try {\n        input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH);\n      } catch (EOFException e) {\n        // If input has less than ID3_HEADER_LENGTH, ignore the rest.\n        break;\n      }\n      scratch.setPosition(0);\n      if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {\n        // Not an ID3 tag.\n        break;\n      }\n      scratch.skipBytes(3); // Skip major version, minor version and flags.\n      int framesLength = scratch.readSynchSafeInt();\n      int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;\n\n      if (metadata == null) {\n        byte[] id3Data = new byte[tagLength];\n        System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);\n        input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);\n\n        metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);\n      } else {\n        input.advancePeekPosition(framesLength);\n      }\n\n      peekedId3Bytes += tagLength;\n    }\n\n    input.resetPeekPosition();\n    input.advancePeekPosition(peekedId3Bytes);\n    return metadata;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * An MPEG audio frame header.\n */\npublic final class MpegAudioHeader {\n\n  /**\n   * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2\n   * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *\n   * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.\n   * The next power of two size is 4 KiB.\n   */\n  public static final int MAX_FRAME_SIZE_BYTES = 4096;\n\n  private static final String[] MIME_TYPE_BY_LAYER =\n      new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};\n  private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};\n  private static final int[] BITRATE_V1_L1 = {\n    32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,\n    416000, 448000\n  };\n  private static final int[] BITRATE_V2_L1 = {\n    32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,\n    224000, 256000\n  };\n  private static final int[] BITRATE_V1_L2 = {\n    32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,\n    320000, 384000\n  };\n  private static final int[] BITRATE_V1_L3 = {\n    32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,\n    320000\n  };\n  private static final int[] BITRATE_V2 = {\n    8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000,\n    160000\n  };\n\n  /**\n   * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it\n   * is invalid.\n   */\n  public static int getFrameSize(int header) {\n    if ((header & 0xFFE00000) != 0xFFE00000) {\n      return C.LENGTH_UNSET;\n    }\n\n    int version = (header >>> 19) & 3;\n    if (version == 1) {\n      return C.LENGTH_UNSET;\n    }\n\n    int layer = (header >>> 17) & 3;\n    if (layer == 0) {\n      return C.LENGTH_UNSET;\n    }\n\n    int bitrateIndex = (header >>> 12) & 15;\n    if (bitrateIndex == 0 || bitrateIndex == 0xF) {\n      // Disallow \"free\" bitrate.\n      return C.LENGTH_UNSET;\n    }\n\n    int samplingRateIndex = (header >>> 10) & 3;\n    if (samplingRateIndex == 3) {\n      return C.LENGTH_UNSET;\n    }\n\n    int samplingRate = SAMPLING_RATE_V1[samplingRateIndex];\n    if (version == 2) {\n      // Version 2\n      samplingRate /= 2;\n    } else if (version == 0) {\n      // Version 2.5\n      samplingRate /= 4;\n    }\n\n    int bitrate;\n    int padding = (header >>> 9) & 1;\n    if (layer == 3) {\n      // Layer I (layer == 3)\n      bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];\n      return (12 * bitrate / samplingRate + padding) * 4;\n    } else {\n      // Layer II (layer == 2) or III (layer == 1)\n      if (version == 3) {\n        bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];\n      } else {\n        // Version 2 or 2.5.\n        bitrate = BITRATE_V2[bitrateIndex - 1];\n      }\n    }\n\n    if (version == 3) {\n      // Version 1\n      return 144 * bitrate / samplingRate + padding;\n    } else {\n      // Version 2 or 2.5\n      return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding;\n    }\n  }\n\n  /**\n   * Parses {@code headerData}, populating {@code header} with the parsed data.\n   *\n   * @param headerData Header data to parse.\n   * @param header Header to populate with data from {@code headerData}.\n   * @return True if the header was populated. False otherwise, indicating that {@code headerData}\n   *     is not a valid MPEG audio header.\n   */\n  public static boolean populateHeader(int headerData, MpegAudioHeader header) {\n    if ((headerData & 0xFFE00000) != 0xFFE00000) {\n      return false;\n    }\n\n    int version = (headerData >>> 19) & 3;\n    if (version == 1) {\n      return false;\n    }\n\n    int layer = (headerData >>> 17) & 3;\n    if (layer == 0) {\n      return false;\n    }\n\n    int bitrateIndex = (headerData >>> 12) & 15;\n    if (bitrateIndex == 0 || bitrateIndex == 0xF) {\n      // Disallow \"free\" bitrate.\n      return false;\n    }\n\n    int samplingRateIndex = (headerData >>> 10) & 3;\n    if (samplingRateIndex == 3) {\n      return false;\n    }\n\n    int sampleRate = SAMPLING_RATE_V1[samplingRateIndex];\n    if (version == 2) {\n      // Version 2\n      sampleRate /= 2;\n    } else if (version == 0) {\n      // Version 2.5\n      sampleRate /= 4;\n    }\n\n    int padding = (headerData >>> 9) & 1;\n    int bitrate;\n    int frameSize;\n    int samplesPerFrame;\n    if (layer == 3) {\n      // Layer I (layer == 3)\n      bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];\n      frameSize = (12 * bitrate / sampleRate + padding) * 4;\n      samplesPerFrame = 384;\n    } else {\n      // Layer II (layer == 2) or III (layer == 1)\n      if (version == 3) {\n        // Version 1\n        bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];\n        samplesPerFrame = 1152;\n        frameSize = 144 * bitrate / sampleRate + padding;\n      } else {\n        // Version 2 or 2.5.\n        bitrate = BITRATE_V2[bitrateIndex - 1];\n        samplesPerFrame = layer == 1 ? 576 : 1152;\n        frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;\n      }\n    }\n\n    String mimeType = MIME_TYPE_BY_LAYER[3 - layer];\n    int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;\n    header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);\n    return true;\n  }\n\n  /** MPEG audio header version. */\n  public int version;\n  /** The mime type. */\n  @Nullable public String mimeType;\n  /** Size of the frame associated with this header, in bytes. */\n  public int frameSize;\n  /** Sample rate in samples per second. */\n  public int sampleRate;\n  /** Number of audio channels in the frame. */\n  public int channels;\n  /** Bitrate of the frame in bit/s. */\n  public int bitrate;\n  /** Number of samples stored in the frame. */\n  public int samplesPerFrame;\n\n  private void setValues(\n      int version,\n      String mimeType,\n      int frameSize,\n      int sampleRate,\n      int channels,\n      int bitrate,\n      int samplesPerFrame) {\n    this.version = version;\n    this.mimeType = mimeType;\n    this.frameSize = frameSize;\n    this.sampleRate = sampleRate;\n    this.channels = channels;\n    this.bitrate = bitrate;\n    this.samplesPerFrame = samplesPerFrame;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\n/**\n * Holds a position in the stream.\n */\npublic final class PositionHolder {\n\n  /**\n   * The held position.\n   */\n  public long position;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.\n */\npublic interface SeekMap {\n\n  /** A {@link SeekMap} that does not support seeking. */\n  class Unseekable implements SeekMap {\n\n    private final long durationUs;\n    private final SeekPoints startSeekPoints;\n\n    /**\n     * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the\n     *     duration is unknown.\n     */\n    public Unseekable(long durationUs) {\n      this(durationUs, 0);\n    }\n\n    /**\n     * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the\n     *     duration is unknown.\n     * @param startPosition The position (byte offset) of the start of the media.\n     */\n    public Unseekable(long durationUs, long startPosition) {\n      this.durationUs = durationUs;\n      startSeekPoints =\n          new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition));\n    }\n\n    @Override\n    public boolean isSeekable() {\n      return false;\n    }\n\n    @Override\n    public long getDurationUs() {\n      return durationUs;\n    }\n\n    @Override\n    public SeekPoints getSeekPoints(long timeUs) {\n      return startSeekPoints;\n    }\n  }\n\n  /** Contains one or two {@link SeekPoint}s. */\n  final class SeekPoints {\n\n    /** The first seek point. */\n    public final SeekPoint first;\n    /** The second seek point, or {@link #first} if there's only one seek point. */\n    public final SeekPoint second;\n\n    /** @param point The single seek point. */\n    public SeekPoints(SeekPoint point) {\n      this(point, point);\n    }\n\n    /**\n     * @param first The first seek point.\n     * @param second The second seek point.\n     */\n    public SeekPoints(SeekPoint first, SeekPoint second) {\n      this.first = Assertions.checkNotNull(first);\n      this.second = Assertions.checkNotNull(second);\n    }\n\n    @Override\n    public String toString() {\n      return \"[\" + first + (first.equals(second) ? \"\" : (\", \" + second)) + \"]\";\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      SeekPoints other = (SeekPoints) obj;\n      return first.equals(other.first) && second.equals(other.second);\n    }\n\n    @Override\n    public int hashCode() {\n      return (31 * first.hashCode()) + second.hashCode();\n    }\n  }\n\n  /**\n   * Returns whether seeking is supported.\n   *\n   * @return Whether seeking is supported.\n   */\n  boolean isSeekable();\n\n  /**\n   * Returns the duration of the stream in microseconds.\n   *\n   * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is\n   *     unknown.\n   */\n  long getDurationUs();\n\n  /**\n   * Obtains seek points for the specified seek time in microseconds. The returned {@link\n   * SeekPoints} will contain one or two distinct seek points.\n   *\n   * <p>Two seek points [A, B] are returned in the case that seeking can only be performed to\n   * discrete points in time, there does not exist a seek point at exactly the requested time, and\n   * there exist seek points on both sides of it. In this case A and B are the closest seek points\n   * before and after the requested time. A single seek point is returned in all other cases.\n   *\n   * @param timeUs A seek time in microseconds.\n   * @return The corresponding seek points.\n   */\n  SeekPoints getSeekPoints(long timeUs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\n\n/** Defines a seek point in a media stream. */\npublic final class SeekPoint {\n\n  /** A {@link SeekPoint} whose time and byte offset are both set to 0. */\n  public static final SeekPoint START = new SeekPoint(0, 0);\n\n  /** The time of the seek point, in microseconds. */\n  public final long timeUs;\n\n  /** The byte offset of the seek point. */\n  public final long position;\n\n  /**\n   * @param timeUs The time of the seek point, in microseconds.\n   * @param position The byte offset of the seek point.\n   */\n  public SeekPoint(long timeUs, long position) {\n    this.timeUs = timeUs;\n    this.position = position;\n  }\n\n  @Override\n  public String toString() {\n    return \"[timeUs=\" + timeUs + \", position=\" + position + \"]\";\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    SeekPoint other = (SeekPoint) obj;\n    return timeUs == other.timeUs && position == other.position;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = (int) timeUs;\n    result = 31 * result + (int) position;\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Arrays;\n\n/**\n * Receives track level data extracted by an {@link Extractor}.\n */\npublic interface TrackOutput {\n\n  /**\n   * Holds data required to decrypt a sample.\n   */\n  final class CryptoData {\n\n    /**\n     * The encryption mode used for the sample.\n     */\n    @C.CryptoMode public final int cryptoMode;\n\n    /**\n     * The encryption key associated with the sample. Its contents must not be modified.\n     */\n    public final byte[] encryptionKey;\n\n    /**\n     * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not\n     * apply.\n     */\n    public final int encryptedBlocks;\n\n    /**\n     * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not\n     * apply.\n     */\n    public final int clearBlocks;\n\n    /**\n     * @param cryptoMode See {@link #cryptoMode}.\n     * @param encryptionKey See {@link #encryptionKey}.\n     * @param encryptedBlocks See {@link #encryptedBlocks}.\n     * @param clearBlocks See {@link #clearBlocks}.\n     */\n    public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks,\n        int clearBlocks) {\n      this.cryptoMode = cryptoMode;\n      this.encryptionKey = encryptionKey;\n      this.encryptedBlocks = encryptedBlocks;\n      this.clearBlocks = clearBlocks;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      CryptoData other = (CryptoData) obj;\n      return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks\n          && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = cryptoMode;\n      result = 31 * result + Arrays.hashCode(encryptionKey);\n      result = 31 * result + encryptedBlocks;\n      result = 31 * result + clearBlocks;\n      return result;\n    }\n\n  }\n\n  /**\n   * Called when the {@link Format} of the track has been extracted from the stream.\n   *\n   * @param format The extracted {@link Format}.\n   */\n  void format(Format format);\n\n  /**\n   * Called to write sample data to the output.\n   *\n   * @param input An {@link ExtractorInput} from which to read the sample data.\n   * @param length The maximum length to read from the input.\n   * @param allowEndOfInput True if encountering the end of the input having read no data is\n   *     allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it\n   *     should be considered an error, causing an {@link EOFException} to be thrown.\n   * @return The number of bytes appended.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException;\n\n  /**\n   * Called to write sample data to the output.\n   *\n   * @param data A {@link ParsableByteArray} from which to read the sample data.\n   * @param length The number of bytes to read, starting from {@code data.getPosition()}.\n   */\n  void sampleData(ParsableByteArray data, int length);\n\n  /**\n   * Called when metadata associated with a sample has been extracted from the stream.\n   *\n   * <p>The corresponding sample data will have already been passed to the output via calls to\n   * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray,\n   * int)}.\n   *\n   * @param timeUs The media timestamp associated with the sample, in microseconds.\n   * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}.\n   * @param size The size of the sample data, in bytes.\n   * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput,\n   *     int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging\n   *     to the sample whose metadata is being passed.\n   * @param encryptionData The encryption data required to decrypt the sample. May be null.\n   */\n  void sampleMetadata(\n          long timeUs,\n          @C.BufferFlags int flags,\n          int size,\n          int offset,\n          @Nullable CryptoData encryptionData);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.amr;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Arrays;\n\n/**\n * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867,\n * section 5.\n *\n * <p>This extractor only supports single-channel AMR container formats.\n */\npublic final class AmrExtractor implements Extractor {\n\n  /** Factory for {@link AmrExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag value is {@link\n   * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})\n  public @interface Flags {}\n  /**\n   * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would\n   * otherwise not be possible.\n   */\n  public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;\n\n  /**\n   * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR\n   * narrow band.\n   */\n  private static final int[] frameSizeBytesByTypeNb = {\n    13,\n    14,\n    16,\n    18,\n    20,\n    21,\n    27,\n    32,\n    6, // AMR SID\n    7, // GSM-EFR SID\n    6, // TDMA-EFR SID\n    6, // PDC-EFR SID\n    1, // Future use\n    1, // Future use\n    1, // Future use\n    1 // No data\n  };\n\n  /**\n   * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide\n   * band.\n   */\n  private static final int[] frameSizeBytesByTypeWb = {\n    18,\n    24,\n    33,\n    37,\n    41,\n    47,\n    51,\n    59,\n    61,\n    6, // AMR-WB SID\n    1, // Future use\n    1, // Future use\n    1, // Future use\n    1, // Future use\n    1, // speech lost\n    1 // No data\n  };\n\n  private static final byte[] amrSignatureNb = Util.getUtf8Bytes(\"#!AMR\\n\");\n  private static final byte[] amrSignatureWb = Util.getUtf8Bytes(\"#!AMR-WB\\n\");\n\n  /** Theoretical maximum frame size for a AMR frame. */\n  private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];\n  /**\n   * The required number of samples in the stream with same sample size to classify the stream as a\n   * constant-bitrate-stream.\n   */\n  private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20;\n\n  private static final int SAMPLE_RATE_WB = 16_000;\n  private static final int SAMPLE_RATE_NB = 8_000;\n  private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;\n\n  private final byte[] scratch;\n  private final @Flags int flags;\n\n  private boolean isWideBand;\n  private long currentSampleTimeUs;\n  private int currentSampleSize;\n  private int currentSampleBytesRemaining;\n  private boolean hasOutputSeekMap;\n  private long firstSamplePosition;\n  private int firstSampleSize;\n  private int numSamplesWithSameSize;\n  private long timeOffsetUs;\n\n  private ExtractorOutput extractorOutput;\n  private TrackOutput trackOutput;\n  @Nullable private SeekMap seekMap;\n  private boolean hasOutputFormat;\n\n  public AmrExtractor() {\n    this(/* flags= */ 0);\n  }\n\n  /** @param flags Flags that control the extractor's behavior. */\n  public AmrExtractor(@Flags int flags) {\n    this.flags = flags;\n    scratch = new byte[1];\n    firstSampleSize = C.LENGTH_UNSET;\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return readAmrHeader(input);\n  }\n\n  @Override\n  public void init(ExtractorOutput extractorOutput) {\n    this.extractorOutput = extractorOutput;\n    trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);\n    extractorOutput.endTracks();\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    if (input.getPosition() == 0) {\n      if (!readAmrHeader(input)) {\n        throw new ParserException(\"Could not find AMR header.\");\n      }\n    }\n    maybeOutputFormat();\n    int sampleReadResult = readSample(input);\n    maybeOutputSeekMap(input.getLength(), sampleReadResult);\n    return sampleReadResult;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    currentSampleTimeUs = 0;\n    currentSampleSize = 0;\n    currentSampleBytesRemaining = 0;\n    if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {\n      timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);\n    } else {\n      timeOffsetUs = 0;\n    }\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  /* package */ static int frameSizeBytesByTypeNb(int frameType) {\n    return frameSizeBytesByTypeNb[frameType];\n  }\n\n  /* package */ static int frameSizeBytesByTypeWb(int frameType) {\n    return frameSizeBytesByTypeWb[frameType];\n  }\n\n  /* package */ static byte[] amrSignatureNb() {\n    return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length);\n  }\n\n  /* package */ static byte[] amrSignatureWb() {\n    return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length);\n  }\n\n  // Internal methods.\n\n  /**\n   * Peeks the AMR header from the beginning of the input, and consumes it if it exists.\n   *\n   * @param input The {@link ExtractorInput} from which data should be peeked/read.\n   * @return Whether the AMR header has been read.\n   */\n  private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException {\n    if (peekAmrSignature(input, amrSignatureNb)) {\n      isWideBand = false;\n      input.skipFully(amrSignatureNb.length);\n      return true;\n    } else if (peekAmrSignature(input, amrSignatureWb)) {\n      isWideBand = true;\n      input.skipFully(amrSignatureWb.length);\n      return true;\n    }\n    return false;\n  }\n\n  /** Peeks from the beginning of the input to see if the given AMR signature exists. */\n  private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature)\n      throws IOException, InterruptedException {\n    input.resetPeekPosition();\n    byte[] header = new byte[amrSignature.length];\n    input.peekFully(header, 0, amrSignature.length);\n    return Arrays.equals(header, amrSignature);\n  }\n\n  private void maybeOutputFormat() {\n    if (!hasOutputFormat) {\n      hasOutputFormat = true;\n      String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB;\n      int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB;\n      trackOutput.format(\n          Format.createAudioSampleFormat(\n              /* id= */ null,\n              mimeType,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              MAX_FRAME_SIZE_BYTES,\n              /* channelCount= */ 1,\n              sampleRate,\n              /* pcmEncoding= */ Format.NO_VALUE,\n              /* initializationData= */ null,\n              /* drmInitData= */ null,\n              /* selectionFlags= */ 0,\n              /* language= */ null));\n    }\n  }\n\n  private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {\n    if (currentSampleBytesRemaining == 0) {\n      try {\n        currentSampleSize = peekNextSampleSize(extractorInput);\n      } catch (EOFException e) {\n        return RESULT_END_OF_INPUT;\n      }\n      currentSampleBytesRemaining = currentSampleSize;\n      if (firstSampleSize == C.LENGTH_UNSET) {\n        firstSamplePosition = extractorInput.getPosition();\n        firstSampleSize = currentSampleSize;\n      }\n      if (firstSampleSize == currentSampleSize) {\n        numSamplesWithSameSize++;\n      }\n    }\n\n    int bytesAppended =\n        trackOutput.sampleData(\n            extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true);\n    if (bytesAppended == C.RESULT_END_OF_INPUT) {\n      return RESULT_END_OF_INPUT;\n    }\n    currentSampleBytesRemaining -= bytesAppended;\n    if (currentSampleBytesRemaining > 0) {\n      return RESULT_CONTINUE;\n    }\n\n    trackOutput.sampleMetadata(\n        timeOffsetUs + currentSampleTimeUs,\n        C.BUFFER_FLAG_KEY_FRAME,\n        currentSampleSize,\n        /* offset= */ 0,\n        /* encryptionData= */ null);\n    currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;\n    return RESULT_CONTINUE;\n  }\n\n  private int peekNextSampleSize(ExtractorInput extractorInput)\n      throws IOException, InterruptedException {\n    extractorInput.resetPeekPosition();\n    extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);\n\n    byte frameHeader = scratch[0];\n    if ((frameHeader & 0x83) > 0) {\n      // The padding bits are at bit-1 positions in the following pattern: 1000 0011\n      // Padding bits must be 0.\n      throw new ParserException(\"Invalid padding bits for frame header \" + frameHeader);\n    }\n\n    int frameType = (frameHeader >> 3) & 0x0f;\n    return getFrameSizeInBytes(frameType);\n  }\n\n  private int getFrameSizeInBytes(int frameType) throws ParserException {\n    if (!isValidFrameType(frameType)) {\n      throw new ParserException(\n          \"Illegal AMR \" + (isWideBand ? \"WB\" : \"NB\") + \" frame type \" + frameType);\n    }\n\n    return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType];\n  }\n\n  private boolean isValidFrameType(int frameType) {\n    return frameType >= 0\n        && frameType <= 15\n        && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType));\n  }\n\n  private boolean isWideBandValidFrameType(int frameType) {\n    // For wide band, type 10-13 are for future use.\n    return isWideBand && (frameType < 10 || frameType > 13);\n  }\n\n  private boolean isNarrowBandValidFrameType(int frameType) {\n    // For narrow band, type 12-14 are for future use.\n    return !isWideBand && (frameType < 12 || frameType > 14);\n  }\n\n  private void maybeOutputSeekMap(long inputLength, int sampleReadResult) {\n    if (hasOutputSeekMap) {\n      return;\n    }\n\n    if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0\n        || inputLength == C.LENGTH_UNSET\n        || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) {\n      seekMap = new SeekMap.Unseekable(C.TIME_UNSET);\n      extractorOutput.seekMap(seekMap);\n      hasOutputSeekMap = true;\n    } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD\n        || sampleReadResult == RESULT_END_OF_INPUT) {\n      seekMap = getConstantBitrateSeekMap(inputLength);\n      extractorOutput.seekMap(seekMap);\n      hasOutputSeekMap = true;\n    }\n  }\n\n  private SeekMap getConstantBitrateSeekMap(long inputLength) {\n    int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);\n    return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize);\n  }\n\n  /**\n   * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.\n   *\n   * @param frameSize The size of each frame in the stream.\n   * @param durationUsPerFrame The duration of the given frame in microseconds.\n   * @return The stream bitrate.\n   */\n  private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {\n    return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.flv;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Collections;\n\n/**\n * Parses audio tags from an FLV stream and extracts AAC frames.\n */\n/* package */ final class AudioTagPayloadReader extends TagPayloadReader {\n\n  private static final int AUDIO_FORMAT_MP3 = 2;\n  private static final int AUDIO_FORMAT_ALAW = 7;\n  private static final int AUDIO_FORMAT_ULAW = 8;\n  private static final int AUDIO_FORMAT_AAC = 10;\n\n  private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;\n  private static final int AAC_PACKET_TYPE_AAC_RAW = 1;\n\n  private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100};\n\n  // State variables\n  private boolean hasParsedAudioDataHeader;\n  private boolean hasOutputFormat;\n  private int audioFormat;\n\n  public AudioTagPayloadReader(TrackOutput output) {\n    super(output);\n  }\n\n  @Override\n  public void seek() {\n    // Do nothing.\n  }\n\n  @Override\n  protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {\n    if (!hasParsedAudioDataHeader) {\n      int header = data.readUnsignedByte();\n      audioFormat = (header >> 4) & 0x0F;\n      if (audioFormat == AUDIO_FORMAT_MP3) {\n        int sampleRateIndex = (header >> 2) & 0x03;\n        int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex];\n        Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null,\n            Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null);\n        output.format(format);\n        hasOutputFormat = true;\n      } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {\n        String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW\n            : MimeTypes.AUDIO_MLAW;\n        int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;\n        Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,\n            Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);\n        output.format(format);\n        hasOutputFormat = true;\n      } else if (audioFormat != AUDIO_FORMAT_AAC) {\n        throw new UnsupportedFormatException(\"Audio format not supported: \" + audioFormat);\n      }\n      hasParsedAudioDataHeader = true;\n    } else {\n      // Skip header if it was parsed previously.\n      data.skipBytes(1);\n    }\n    return true;\n  }\n\n  @Override\n  protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {\n    if (audioFormat == AUDIO_FORMAT_MP3) {\n      int sampleSize = data.bytesLeft();\n      output.sampleData(data, sampleSize);\n      output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n      return true;\n    } else {\n      int packetType = data.readUnsignedByte();\n      if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {\n        // Parse the sequence header.\n        byte[] audioSpecificConfig = new byte[data.bytesLeft()];\n        data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length);\n        Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(\n            audioSpecificConfig);\n        Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,\n            Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,\n            Collections.singletonList(audioSpecificConfig), null, 0, null);\n        output.format(format);\n        hasOutputFormat = true;\n        return false;\n      } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {\n        int sampleSize = data.bytesLeft();\n        output.sampleData(data, sampleSize);\n        output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n        return true;\n      } else {\n        return false;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.flv;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Extracts data from the FLV container format.\n */\npublic final class FlvExtractor implements Extractor {\n\n  /** Factory for {@link FlvExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()};\n\n  /** Extractor states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    STATE_READING_FLV_HEADER,\n    STATE_SKIPPING_TO_TAG_HEADER,\n    STATE_READING_TAG_HEADER,\n    STATE_READING_TAG_DATA\n  })\n  private @interface States {}\n\n  private static final int STATE_READING_FLV_HEADER = 1;\n  private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;\n  private static final int STATE_READING_TAG_HEADER = 3;\n  private static final int STATE_READING_TAG_DATA = 4;\n\n  // Header sizes.\n  private static final int FLV_HEADER_SIZE = 9;\n  private static final int FLV_TAG_HEADER_SIZE = 11;\n\n  // Tag types.\n  private static final int TAG_TYPE_AUDIO = 8;\n  private static final int TAG_TYPE_VIDEO = 9;\n  private static final int TAG_TYPE_SCRIPT_DATA = 18;\n\n  // FLV container identifier.\n  private static final int FLV_TAG = 0x00464c56;\n\n  private final ParsableByteArray scratch;\n  private final ParsableByteArray headerBuffer;\n  private final ParsableByteArray tagHeaderBuffer;\n  private final ParsableByteArray tagData;\n  private final ScriptTagPayloadReader metadataReader;\n\n  private ExtractorOutput extractorOutput;\n  private @States int state;\n  private boolean outputFirstSample;\n  private long mediaTagTimestampOffsetUs;\n  private int bytesToNextTagHeader;\n  private int tagType;\n  private int tagDataSize;\n  private long tagTimestampUs;\n  private boolean outputSeekMap;\n  private AudioTagPayloadReader audioReader;\n  private VideoTagPayloadReader videoReader;\n\n  public FlvExtractor() {\n    scratch = new ParsableByteArray(4);\n    headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);\n    tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);\n    tagData = new ParsableByteArray();\n    metadataReader = new ScriptTagPayloadReader();\n    state = STATE_READING_FLV_HEADER;\n  }\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    // Check if file starts with \"FLV\" tag\n    input.peekFully(scratch.data, 0, 3);\n    scratch.setPosition(0);\n    if (scratch.readUnsignedInt24() != FLV_TAG) {\n      return false;\n    }\n\n    // Checking reserved flags are set to 0\n    input.peekFully(scratch.data, 0, 2);\n    scratch.setPosition(0);\n    if ((scratch.readUnsignedShort() & 0xFA) != 0) {\n      return false;\n    }\n\n    // Read data offset\n    input.peekFully(scratch.data, 0, 4);\n    scratch.setPosition(0);\n    int dataOffset = scratch.readInt();\n\n    input.resetPeekPosition();\n    input.advancePeekPosition(dataOffset);\n\n    // Checking first \"previous tag size\" is set to 0\n    input.peekFully(scratch.data, 0, 4);\n    scratch.setPosition(0);\n\n    return scratch.readInt() == 0;\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.extractorOutput = output;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    state = STATE_READING_FLV_HEADER;\n    outputFirstSample = false;\n    bytesToNextTagHeader = 0;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,\n      InterruptedException {\n    while (true) {\n      switch (state) {\n        case STATE_READING_FLV_HEADER:\n          if (!readFlvHeader(input)) {\n            return RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_SKIPPING_TO_TAG_HEADER:\n          skipToTagHeader(input);\n          break;\n        case STATE_READING_TAG_HEADER:\n          if (!readTagHeader(input)) {\n            return RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_READING_TAG_DATA:\n          if (readTagData(input)) {\n            return RESULT_CONTINUE;\n          }\n          break;\n        default:\n          // Never happens.\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  /**\n   * Reads an FLV container header from the provided {@link ExtractorInput}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @return True if header was read successfully. False if the end of stream was reached.\n   * @throws IOException If an error occurred reading or parsing data from the source.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {\n    if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {\n      // We've reached the end of the stream.\n      return false;\n    }\n\n    headerBuffer.setPosition(0);\n    headerBuffer.skipBytes(4);\n    int flags = headerBuffer.readUnsignedByte();\n    boolean hasAudio = (flags & 0x04) != 0;\n    boolean hasVideo = (flags & 0x01) != 0;\n    if (hasAudio && audioReader == null) {\n      audioReader = new AudioTagPayloadReader(\n          extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));\n    }\n    if (hasVideo && videoReader == null) {\n      videoReader = new VideoTagPayloadReader(\n          extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));\n    }\n    extractorOutput.endTracks();\n\n    // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.\n    bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;\n    state = STATE_SKIPPING_TO_TAG_HEADER;\n    return true;\n  }\n\n  /**\n   * Skips over data to reach the next tag header.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @throws IOException If an error occurred skipping data from the source.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {\n    input.skipFully(bytesToNextTagHeader);\n    bytesToNextTagHeader = 0;\n    state = STATE_READING_TAG_HEADER;\n  }\n\n  /**\n   * Reads a tag header from the provided {@link ExtractorInput}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @return True if tag header was read successfully. Otherwise, false.\n   * @throws IOException If an error occurred reading or parsing data from the source.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {\n    if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {\n      // We've reached the end of the stream.\n      return false;\n    }\n\n    tagHeaderBuffer.setPosition(0);\n    tagType = tagHeaderBuffer.readUnsignedByte();\n    tagDataSize = tagHeaderBuffer.readUnsignedInt24();\n    tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();\n    tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;\n    tagHeaderBuffer.skipBytes(3); // streamId\n    state = STATE_READING_TAG_DATA;\n    return true;\n  }\n\n  /**\n   * Reads the body of a tag from the provided {@link ExtractorInput}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @return True if the data was consumed by a reader. False if it was skipped.\n   * @throws IOException If an error occurred reading or parsing data from the source.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {\n    boolean wasConsumed = true;\n    boolean wasSampleOutput = false;\n    long timestampUs = getCurrentTimestampUs();\n    if (tagType == TAG_TYPE_AUDIO && audioReader != null) {\n      ensureReadyForMediaOutput();\n      wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);\n    } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {\n      ensureReadyForMediaOutput();\n      wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs);\n    } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {\n      wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);\n      long durationUs = metadataReader.getDurationUs();\n      if (durationUs != C.TIME_UNSET) {\n        extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));\n        outputSeekMap = true;\n      }\n    } else {\n      input.skipFully(tagDataSize);\n      wasConsumed = false;\n    }\n    if (!outputFirstSample && wasSampleOutput) {\n      outputFirstSample = true;\n      mediaTagTimestampOffsetUs =\n          metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;\n    }\n    bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.\n    state = STATE_SKIPPING_TO_TAG_HEADER;\n    return wasConsumed;\n  }\n\n  private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,\n      InterruptedException {\n    if (tagDataSize > tagData.capacity()) {\n      tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0);\n    } else {\n      tagData.setPosition(0);\n    }\n    tagData.setLimit(tagDataSize);\n    input.readFully(tagData.data, 0, tagDataSize);\n    return tagData;\n  }\n\n  private void ensureReadyForMediaOutput() {\n    if (!outputSeekMap) {\n      extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));\n      outputSeekMap = true;\n    }\n  }\n\n  private long getCurrentTimestampUs() {\n    return outputFirstSample\n        ? (mediaTagTimestampOffsetUs + tagTimestampUs)\n        : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.flv;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.DummyTrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Parses Script Data tags from an FLV stream and extracts metadata information.\n */\n/* package */ final class ScriptTagPayloadReader extends TagPayloadReader {\n\n  private static final String NAME_METADATA = \"onMetaData\";\n  private static final String KEY_DURATION = \"duration\";\n\n  // AMF object types\n  private static final int AMF_TYPE_NUMBER = 0;\n  private static final int AMF_TYPE_BOOLEAN = 1;\n  private static final int AMF_TYPE_STRING = 2;\n  private static final int AMF_TYPE_OBJECT = 3;\n  private static final int AMF_TYPE_ECMA_ARRAY = 8;\n  private static final int AMF_TYPE_END_MARKER = 9;\n  private static final int AMF_TYPE_STRICT_ARRAY = 10;\n  private static final int AMF_TYPE_DATE = 11;\n\n  private long durationUs;\n\n  public ScriptTagPayloadReader() {\n    super(new DummyTrackOutput());\n    durationUs = C.TIME_UNSET;\n  }\n\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  @Override\n  public void seek() {\n    // Do nothing.\n  }\n\n  @Override\n  protected boolean parseHeader(ParsableByteArray data) {\n    return true;\n  }\n\n  @Override\n  protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {\n    int nameType = readAmfType(data);\n    if (nameType != AMF_TYPE_STRING) {\n      // Should never happen.\n      throw new ParserException();\n    }\n    String name = readAmfString(data);\n    if (!NAME_METADATA.equals(name)) {\n      // We're only interested in metadata.\n      return false;\n    }\n    int type = readAmfType(data);\n    if (type != AMF_TYPE_ECMA_ARRAY) {\n      // We're not interested in this metadata.\n      return false;\n    }\n    // Set the duration to the value contained in the metadata, if present.\n    Map<String, Object> metadata = readAmfEcmaArray(data);\n    if (metadata.containsKey(KEY_DURATION)) {\n      double durationSeconds = (double) metadata.get(KEY_DURATION);\n      if (durationSeconds > 0.0) {\n        durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);\n      }\n    }\n    return false;\n  }\n\n  private static int readAmfType(ParsableByteArray data) {\n    return data.readUnsignedByte();\n  }\n\n  /**\n   * Read a boolean from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static Boolean readAmfBoolean(ParsableByteArray data) {\n    return data.readUnsignedByte() == 1;\n  }\n\n  /**\n   * Read a double number from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static Double readAmfDouble(ParsableByteArray data) {\n    return Double.longBitsToDouble(data.readLong());\n  }\n\n  /**\n   * Read a string from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static String readAmfString(ParsableByteArray data) {\n    int size = data.readUnsignedShort();\n    int position = data.getPosition();\n    data.skipBytes(size);\n    return new String(data.data, position, size);\n  }\n\n  /**\n   * Read an array from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {\n    int count = data.readUnsignedIntToInt();\n    ArrayList<Object> list = new ArrayList<>(count);\n    for (int i = 0; i < count; i++) {\n      int type = readAmfType(data);\n      Object value = readAmfData(data, type);\n      if (value != null) {\n        list.add(value);\n      }\n    }\n    return list;\n  }\n\n  /**\n   * Read an object from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {\n    HashMap<String, Object> array = new HashMap<>();\n    while (true) {\n      String key = readAmfString(data);\n      int type = readAmfType(data);\n      if (type == AMF_TYPE_END_MARKER) {\n        break;\n      }\n      Object value = readAmfData(data, type);\n      if (value != null) {\n        array.put(key, value);\n      }\n    }\n    return array;\n  }\n\n  /**\n   * Read an ECMA array from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {\n    int count = data.readUnsignedIntToInt();\n    HashMap<String, Object> array = new HashMap<>(count);\n    for (int i = 0; i < count; i++) {\n      String key = readAmfString(data);\n      int type = readAmfType(data);\n      Object value = readAmfData(data, type);\n      if (value != null) {\n        array.put(key, value);\n      }\n    }\n    return array;\n  }\n\n  /**\n   * Read a date from an AMF encoded buffer.\n   *\n   * @param data The buffer from which to read.\n   * @return The value read from the buffer.\n   */\n  private static Date readAmfDate(ParsableByteArray data) {\n    Date date = new Date((long) readAmfDouble(data).doubleValue());\n    data.skipBytes(2); // Skip reserved bytes.\n    return date;\n  }\n\n  @Nullable\n  private static Object readAmfData(ParsableByteArray data, int type) {\n    switch (type) {\n      case AMF_TYPE_NUMBER:\n        return readAmfDouble(data);\n      case AMF_TYPE_BOOLEAN:\n        return readAmfBoolean(data);\n      case AMF_TYPE_STRING:\n        return readAmfString(data);\n      case AMF_TYPE_OBJECT:\n        return readAmfObject(data);\n      case AMF_TYPE_ECMA_ARRAY:\n        return readAmfEcmaArray(data);\n      case AMF_TYPE_STRICT_ARRAY:\n        return readAmfStrictArray(data);\n      case AMF_TYPE_DATE:\n        return readAmfDate(data);\n      default:\n        // We don't log a warning because there are types that we knowingly don't support.\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.flv;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Extracts individual samples from FLV tags, preserving original order.\n */\n/* package */ abstract class TagPayloadReader {\n\n  /**\n   * Thrown when the format is not supported.\n   */\n  public static final class UnsupportedFormatException extends ParserException {\n\n    public UnsupportedFormatException(String msg) {\n      super(msg);\n    }\n\n  }\n\n  protected final TrackOutput output;\n\n  /**\n   * @param output A {@link TrackOutput} to which samples should be written.\n   */\n  protected TagPayloadReader(TrackOutput output) {\n    this.output = output;\n  }\n\n  /**\n   * Notifies the reader that a seek has occurred.\n   * <p>\n   * Following a call to this method, the data passed to the next invocation of\n   * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that\n   * was previously passed. Hence the reader should reset any internal state.\n   */\n  public abstract void seek();\n\n  /**\n   * Consumes payload data.\n   *\n   * @param data The payload data to consume.\n   * @param timeUs The timestamp associated with the payload.\n   * @return Whether a sample was output.\n   * @throws ParserException If an error occurs parsing the data.\n   */\n  public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException {\n    return parseHeader(data) && parsePayload(data, timeUs);\n  }\n\n  /**\n   * Parses tag header.\n   *\n   * @param data Buffer where the tag header is stored.\n   * @return Whether the header was parsed successfully.\n   * @throws ParserException If an error occurs parsing the header.\n   */\n  protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException;\n\n  /**\n   * Parses tag payload.\n   *\n   * @param data Buffer where tag payload is stored.\n   * @param timeUs Time position of the frame.\n   * @return Whether a sample was output.\n   * @throws ParserException If an error occurs parsing the payload.\n   */\n  protected abstract boolean parsePayload(ParsableByteArray data, long timeUs)\n      throws ParserException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.flv;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.video.AvcConfig;\n\n/**\n * Parses video tags from an FLV stream and extracts H.264 nal units.\n */\n/* package */ final class VideoTagPayloadReader extends TagPayloadReader {\n\n  // Video codec.\n  private static final int VIDEO_CODEC_AVC = 7;\n\n  // Frame types.\n  private static final int VIDEO_FRAME_KEYFRAME = 1;\n  private static final int VIDEO_FRAME_VIDEO_INFO = 5;\n\n  // Packet types.\n  private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;\n  private static final int AVC_PACKET_TYPE_AVC_NALU = 1;\n\n  // Temporary arrays.\n  private final ParsableByteArray nalStartCode;\n  private final ParsableByteArray nalLength;\n  private int nalUnitLengthFieldLength;\n\n  // State variables.\n  private boolean hasOutputFormat;\n  private boolean hasOutputKeyframe;\n  private int frameType;\n\n  /**\n   * @param output A {@link TrackOutput} to which samples should be written.\n   */\n  public VideoTagPayloadReader(TrackOutput output) {\n    super(output);\n    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);\n    nalLength = new ParsableByteArray(4);\n  }\n\n  @Override\n  public void seek() {\n    hasOutputKeyframe = false;\n  }\n\n  @Override\n  protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {\n    int header = data.readUnsignedByte();\n    int frameType = (header >> 4) & 0x0F;\n    int videoCodec = (header & 0x0F);\n    // Support just H.264 encoded content.\n    if (videoCodec != VIDEO_CODEC_AVC) {\n      throw new UnsupportedFormatException(\"Video format not supported: \" + videoCodec);\n    }\n    this.frameType = frameType;\n    return (frameType != VIDEO_FRAME_VIDEO_INFO);\n  }\n\n  @Override\n  protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {\n    int packetType = data.readUnsignedByte();\n    int compositionTimeMs = data.readInt24();\n\n    timeUs += compositionTimeMs * 1000L;\n    // Parse avc sequence header in case this was not done before.\n    if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {\n      ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);\n      data.readBytes(videoSequence.data, 0, data.bytesLeft());\n      AvcConfig avcConfig = AvcConfig.parse(videoSequence);\n      nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;\n      // Construct and output the format.\n      Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,\n          Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE,\n          avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null);\n      output.format(format);\n      hasOutputFormat = true;\n      return false;\n    } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) {\n      boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME;\n      if (!hasOutputKeyframe && !isKeyframe) {\n        return false;\n      }\n      // TODO: Deduplicate with Mp4Extractor.\n      // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case\n      // they're only 1 or 2 bytes long.\n      byte[] nalLengthData = nalLength.data;\n      nalLengthData[0] = 0;\n      nalLengthData[1] = 0;\n      nalLengthData[2] = 0;\n      int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength;\n      // NAL units are length delimited, but the decoder requires start code delimited units.\n      // Loop until we've written the sample to the track output, replacing length delimiters with\n      // start codes as we encounter them.\n      int bytesWritten = 0;\n      int bytesToWrite;\n      while (data.bytesLeft() > 0) {\n        // Read the NAL length so that we know where we find the next one.\n        data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);\n        nalLength.setPosition(0);\n        bytesToWrite = nalLength.readUnsignedIntToInt();\n\n        // Write a start code for the current NAL unit.\n        nalStartCode.setPosition(0);\n        output.sampleData(nalStartCode, 4);\n        bytesWritten += 4;\n\n        // Write the payload of the NAL unit.\n        output.sampleData(data, bytesToWrite);\n        bytesWritten += bytesToWrite;\n      }\n      output.sampleMetadata(\n          timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null);\n      hasOutputKeyframe = true;\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayDeque;\n\n/**\n * Default implementation of {@link EbmlReader}.\n */\n/* package */ final class DefaultEbmlReader implements EbmlReader {\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT})\n  private @interface ElementState {}\n\n  private static final int ELEMENT_STATE_READ_ID = 0;\n  private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;\n  private static final int ELEMENT_STATE_READ_CONTENT = 2;\n\n  private static final int MAX_ID_BYTES = 4;\n  private static final int MAX_LENGTH_BYTES = 8;\n\n  private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;\n  private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;\n  private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;\n\n  private final byte[] scratch;\n  private final ArrayDeque<MasterElement> masterElementsStack;\n  private final VarintReader varintReader;\n\n  private EbmlProcessor processor;\n  private @ElementState int elementState;\n  private int elementId;\n  private long elementContentSize;\n\n  public DefaultEbmlReader() {\n    scratch = new byte[8];\n    masterElementsStack = new ArrayDeque<>();\n    varintReader = new VarintReader();\n  }\n\n  @Override\n  public void init(EbmlProcessor processor) {\n    this.processor = processor;\n  }\n\n  @Override\n  public void reset() {\n    elementState = ELEMENT_STATE_READ_ID;\n    masterElementsStack.clear();\n    varintReader.reset();\n  }\n\n  @Override\n  public boolean read(ExtractorInput input) throws IOException, InterruptedException {\n    Assertions.checkNotNull(processor);\n    while (true) {\n      if (!masterElementsStack.isEmpty()\n          && input.getPosition() >= masterElementsStack.peek().elementEndPosition) {\n        processor.endMasterElement(masterElementsStack.pop().elementId);\n        return true;\n      }\n\n      if (elementState == ELEMENT_STATE_READ_ID) {\n        long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES);\n        if (result == C.RESULT_MAX_LENGTH_EXCEEDED) {\n          result = maybeResyncToNextLevel1Element(input);\n        }\n        if (result == C.RESULT_END_OF_INPUT) {\n          return false;\n        }\n        // Element IDs are at most 4 bytes, so we can cast to integers.\n        elementId = (int) result;\n        elementState = ELEMENT_STATE_READ_CONTENT_SIZE;\n      }\n\n      if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {\n        elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES);\n        elementState = ELEMENT_STATE_READ_CONTENT;\n      }\n\n      @EbmlProcessor.ElementType int type = processor.getElementType(elementId);\n      switch (type) {\n        case EbmlProcessor.ELEMENT_TYPE_MASTER:\n          long elementContentPosition = input.getPosition();\n          long elementEndPosition = elementContentPosition + elementContentSize;\n          masterElementsStack.push(new MasterElement(elementId, elementEndPosition));\n          processor.startMasterElement(elementId, elementContentPosition, elementContentSize);\n          elementState = ELEMENT_STATE_READ_ID;\n          return true;\n        case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT:\n          if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {\n            throw new ParserException(\"Invalid integer size: \" + elementContentSize);\n          }\n          processor.integerElement(elementId, readInteger(input, (int) elementContentSize));\n          elementState = ELEMENT_STATE_READ_ID;\n          return true;\n        case EbmlProcessor.ELEMENT_TYPE_FLOAT:\n          if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES\n              && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {\n            throw new ParserException(\"Invalid float size: \" + elementContentSize);\n          }\n          processor.floatElement(elementId, readFloat(input, (int) elementContentSize));\n          elementState = ELEMENT_STATE_READ_ID;\n          return true;\n        case EbmlProcessor.ELEMENT_TYPE_STRING:\n          if (elementContentSize > Integer.MAX_VALUE) {\n            throw new ParserException(\"String element size: \" + elementContentSize);\n          }\n          processor.stringElement(elementId, readString(input, (int) elementContentSize));\n          elementState = ELEMENT_STATE_READ_ID;\n          return true;\n        case EbmlProcessor.ELEMENT_TYPE_BINARY:\n          processor.binaryElement(elementId, (int) elementContentSize, input);\n          elementState = ELEMENT_STATE_READ_ID;\n          return true;\n        case EbmlProcessor.ELEMENT_TYPE_UNKNOWN:\n          input.skipFully((int) elementContentSize);\n          elementState = ELEMENT_STATE_READ_ID;\n          break;\n        default:\n          throw new ParserException(\"Invalid element type \" + type);\n      }\n    }\n  }\n\n  /**\n   * Does a byte by byte search to try and find the next level 1 element. This method is called if\n   * some invalid data is encountered in the parser.\n   *\n   * @param input The {@link ExtractorInput} from which data has to be read.\n   * @return id of the next level 1 element that has been found.\n   * @throws EOFException If the end of input was encountered when searching for the next level 1\n   *     element.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException,\n      InterruptedException {\n    input.resetPeekPosition();\n    while (true) {\n      input.peekFully(scratch, 0, MAX_ID_BYTES);\n      int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]);\n      if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) {\n        int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false);\n        if (processor.isLevel1Element(potentialId)) {\n          input.skipFully(varintLength);\n          return potentialId;\n        }\n      }\n      input.skipFully(1);\n    }\n  }\n\n  /**\n   * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @param byteLength The length of the integer being read.\n   * @return The read integer value.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private long readInteger(ExtractorInput input, int byteLength)\n      throws IOException, InterruptedException {\n    input.readFully(scratch, 0, byteLength);\n    long value = 0;\n    for (int i = 0; i < byteLength; i++) {\n      value = (value << 8) | (scratch[i] & 0xFF);\n    }\n    return value;\n  }\n\n  /**\n   * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @param byteLength The length of the float being read.\n   * @return The read float value.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private double readFloat(ExtractorInput input, int byteLength)\n      throws IOException, InterruptedException {\n    long integerValue = readInteger(input, byteLength);\n    double floatValue;\n    if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {\n      floatValue = Float.intBitsToFloat((int) integerValue);\n    } else {\n      floatValue = Double.longBitsToDouble(integerValue);\n    }\n    return floatValue;\n  }\n\n  /**\n   * Reads a string of length {@code byteLength} from the {@link ExtractorInput}. Zero padding is\n   * removed, so the returned string may be shorter than {@code byteLength}.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @param byteLength The length of the string being read, including zero padding.\n   * @return The read string value.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private String readString(ExtractorInput input, int byteLength)\n      throws IOException, InterruptedException {\n    if (byteLength == 0) {\n      return \"\";\n    }\n    byte[] stringBytes = new byte[byteLength];\n    input.readFully(stringBytes, 0, byteLength);\n    // Remove zero padding.\n    int trimmedLength = byteLength;\n    while (trimmedLength > 0 && stringBytes[trimmedLength - 1] == 0) {\n      trimmedLength--;\n    }\n    return new String(stringBytes, 0, trimmedLength);\n  }\n\n  /**\n   * Used in {@link #masterElementsStack} to track when the current master element ends, so that\n   * {@link EbmlProcessor#endMasterElement(int)} can be called.\n   */\n  private static final class MasterElement {\n\n    private final int elementId;\n    private final long elementEndPosition;\n\n    private MasterElement(int elementId, long elementEndPosition) {\n      this.elementId = elementId;\n      this.elementEndPosition = elementEndPosition;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Defines EBML element IDs/types and processes events. */\npublic interface EbmlProcessor {\n\n  /**\n   * EBML element types. One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link\n   * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} or\n   * {@link #ELEMENT_TYPE_FLOAT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    ELEMENT_TYPE_UNKNOWN,\n    ELEMENT_TYPE_MASTER,\n    ELEMENT_TYPE_UNSIGNED_INT,\n    ELEMENT_TYPE_STRING,\n    ELEMENT_TYPE_BINARY,\n    ELEMENT_TYPE_FLOAT\n  })\n  @interface ElementType {}\n  /** Type for unknown elements. */\n  int ELEMENT_TYPE_UNKNOWN = 0;\n  /** Type for elements that contain child elements. */\n  int ELEMENT_TYPE_MASTER = 1;\n  /** Type for integer value elements of up to 8 bytes. */\n  int ELEMENT_TYPE_UNSIGNED_INT = 2;\n  /** Type for string elements. */\n  int ELEMENT_TYPE_STRING = 3;\n  /** Type for binary elements. */\n  int ELEMENT_TYPE_BINARY = 4;\n  /** Type for IEEE floating point value elements of either 4 or 8 bytes. */\n  int ELEMENT_TYPE_FLOAT = 5;\n\n  /**\n   * Maps an element ID to a corresponding type.\n   *\n   * <p>If {@link #ELEMENT_TYPE_UNKNOWN} is returned then the element is skipped. Note that all\n   * children of a skipped element are also skipped.\n   *\n   * @param id The element ID to map.\n   * @return One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link\n   *     #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} and\n   *     {@link #ELEMENT_TYPE_FLOAT}.\n   */\n  @ElementType\n  int getElementType(int id);\n\n  /**\n   * Checks if the given id is that of a level 1 element.\n   *\n   * @param id The element ID.\n   * @return Whether the given id is that of a level 1 element.\n   */\n  boolean isLevel1Element(int id);\n\n  /**\n   * Called when the start of a master element is encountered.\n   * <p>\n   * Following events should be considered as taking place within this element until a matching call\n   * to {@link #endMasterElement(int)} is made.\n   * <p>\n   * Note that it is possible for another master element of the same element ID to be nested within\n   * itself.\n   *\n   * @param id The element ID.\n   * @param contentPosition The position of the start of the element's content in the stream.\n   * @param contentSize The size of the element's content in bytes.\n   * @throws ParserException If a parsing error occurs.\n   */\n  void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;\n\n  /**\n   * Called when the end of a master element is encountered.\n   *\n   * @param id The element ID.\n   * @throws ParserException If a parsing error occurs.\n   */\n  void endMasterElement(int id) throws ParserException;\n\n  /**\n   * Called when an integer element is encountered.\n   *\n   * @param id The element ID.\n   * @param value The integer value that the element contains.\n   * @throws ParserException If a parsing error occurs.\n   */\n  void integerElement(int id, long value) throws ParserException;\n\n  /**\n   * Called when a float element is encountered.\n   *\n   * @param id The element ID.\n   * @param value The float value that the element contains\n   * @throws ParserException If a parsing error occurs.\n   */\n  void floatElement(int id, double value) throws ParserException;\n\n  /**\n   * Called when a string element is encountered.\n   *\n   * @param id The element ID.\n   * @param value The string value that the element contains.\n   * @throws ParserException If a parsing error occurs.\n   */\n  void stringElement(int id, String value) throws ParserException;\n\n  /**\n   * Called when a binary element is encountered.\n   * <p>\n   * The element header (containing the element ID and content size) will already have been read.\n   * Implementations are required to consume the whole remainder of the element, which is\n   * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail\n   * (by throwing an exception) having partially consumed the data, however if they do this, they\n   * must consume the remainder of the content when called again.\n   *\n   * @param id The element ID.\n   * @param contentsSize The element's content size.\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @throws ParserException If a parsing error occurs.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  void binaryElement(int id, int contentsSize, ExtractorInput input)\n      throws IOException, InterruptedException;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport java.io.IOException;\n\n/**\n * Event-driven EBML reader that delivers events to an {@link EbmlProcessor}.\n *\n * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was\n * originally designed for the Matroska container format. More information about EBML and Matroska\n * is available <a href=\"http://www.matroska.org/technical/specs/index.html\">here</a>.\n */\n/* package */ interface EbmlReader {\n\n  /**\n   * Initializes the extractor with an {@link EbmlProcessor}.\n   *\n   * @param processor An {@link EbmlProcessor} to process events.\n   */\n  void init(EbmlProcessor processor);\n\n  /**\n   * Resets the state of the reader.\n   * <p>\n   * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure\n   * from scratch.\n   */\n  void reset();\n\n  /**\n   * Reads from an {@link ExtractorInput}, invoking an event callback if possible.\n   *\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @return True if data can continue to be read. False if the end of the input was encountered.\n   * @throws ParserException If parsing fails.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  boolean read(ExtractorInput input) throws IOException, InterruptedException;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport android.util.Pair;\nimport android.util.SparseArray;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.Ac3Util;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.LongArray;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.AvcConfig;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport com.google.android.exoplayer2.video.HevcConfig;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.UUID;\n\n/** Extracts data from the Matroska and WebM container formats. */\npublic class MatroskaExtractor implements Extractor {\n\n  /** Factory for {@link MatroskaExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag value is {@link\n   * #FLAG_DISABLE_SEEK_FOR_CUES}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_DISABLE_SEEK_FOR_CUES})\n  public @interface Flags {}\n  /**\n   * Flag to disable seeking for cues.\n   * <p>\n   * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its\n   * position is specified in the seek head and if it's after the first cluster. Setting this flag\n   * disables seeking to the cues element. If the cues element is after the first cluster then the\n   * media is treated as being unseekable.\n   */\n  public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1;\n\n  private static final String TAG = \"MatroskaExtractor\";\n\n  private static final int UNSET_ENTRY_ID = -1;\n\n  private static final int BLOCK_STATE_START = 0;\n  private static final int BLOCK_STATE_HEADER = 1;\n  private static final int BLOCK_STATE_DATA = 2;\n\n  private static final String DOC_TYPE_MATROSKA = \"matroska\";\n  private static final String DOC_TYPE_WEBM = \"webm\";\n  private static final String CODEC_ID_VP8 = \"V_VP8\";\n  private static final String CODEC_ID_VP9 = \"V_VP9\";\n  private static final String CODEC_ID_AV1 = \"V_AV1\";\n  private static final String CODEC_ID_MPEG2 = \"V_MPEG2\";\n  private static final String CODEC_ID_MPEG4_SP = \"V_MPEG4/ISO/SP\";\n  private static final String CODEC_ID_MPEG4_ASP = \"V_MPEG4/ISO/ASP\";\n  private static final String CODEC_ID_MPEG4_AP = \"V_MPEG4/ISO/AP\";\n  private static final String CODEC_ID_H264 = \"V_MPEG4/ISO/AVC\";\n  private static final String CODEC_ID_H265 = \"V_MPEGH/ISO/HEVC\";\n  private static final String CODEC_ID_FOURCC = \"V_MS/VFW/FOURCC\";\n  private static final String CODEC_ID_THEORA = \"V_THEORA\";\n  private static final String CODEC_ID_VORBIS = \"A_VORBIS\";\n  private static final String CODEC_ID_OPUS = \"A_OPUS\";\n  private static final String CODEC_ID_AAC = \"A_AAC\";\n  private static final String CODEC_ID_MP2 = \"A_MPEG/L2\";\n  private static final String CODEC_ID_MP3 = \"A_MPEG/L3\";\n  private static final String CODEC_ID_AC3 = \"A_AC3\";\n  private static final String CODEC_ID_E_AC3 = \"A_EAC3\";\n  private static final String CODEC_ID_TRUEHD = \"A_TRUEHD\";\n  private static final String CODEC_ID_DTS = \"A_DTS\";\n  private static final String CODEC_ID_DTS_EXPRESS = \"A_DTS/EXPRESS\";\n  private static final String CODEC_ID_DTS_LOSSLESS = \"A_DTS/LOSSLESS\";\n  private static final String CODEC_ID_FLAC = \"A_FLAC\";\n  private static final String CODEC_ID_ACM = \"A_MS/ACM\";\n  private static final String CODEC_ID_PCM_INT_LIT = \"A_PCM/INT/LIT\";\n  private static final String CODEC_ID_SUBRIP = \"S_TEXT/UTF8\";\n  private static final String CODEC_ID_ASS = \"S_TEXT/ASS\";\n  private static final String CODEC_ID_VOBSUB = \"S_VOBSUB\";\n  private static final String CODEC_ID_PGS = \"S_HDMV/PGS\";\n  private static final String CODEC_ID_DVBSUB = \"S_DVBSUB\";\n\n  private static final int VORBIS_MAX_INPUT_SIZE = 8192;\n  private static final int OPUS_MAX_INPUT_SIZE = 5760;\n  private static final int ENCRYPTION_IV_SIZE = 8;\n  private static final int TRACK_TYPE_AUDIO = 2;\n\n  private static final int ID_EBML = 0x1A45DFA3;\n  private static final int ID_EBML_READ_VERSION = 0x42F7;\n  private static final int ID_DOC_TYPE = 0x4282;\n  private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;\n  private static final int ID_SEGMENT = 0x18538067;\n  private static final int ID_SEGMENT_INFO = 0x1549A966;\n  private static final int ID_SEEK_HEAD = 0x114D9B74;\n  private static final int ID_SEEK = 0x4DBB;\n  private static final int ID_SEEK_ID = 0x53AB;\n  private static final int ID_SEEK_POSITION = 0x53AC;\n  private static final int ID_INFO = 0x1549A966;\n  private static final int ID_TIMECODE_SCALE = 0x2AD7B1;\n  private static final int ID_DURATION = 0x4489;\n  private static final int ID_CLUSTER = 0x1F43B675;\n  private static final int ID_TIME_CODE = 0xE7;\n  private static final int ID_SIMPLE_BLOCK = 0xA3;\n  private static final int ID_BLOCK_GROUP = 0xA0;\n  private static final int ID_BLOCK = 0xA1;\n  private static final int ID_BLOCK_DURATION = 0x9B;\n  private static final int ID_BLOCK_ADDITIONS = 0x75A1;\n  private static final int ID_BLOCK_MORE = 0xA6;\n  private static final int ID_BLOCK_ADD_ID = 0xEE;\n  private static final int ID_BLOCK_ADDITIONAL = 0xA5;\n  private static final int ID_REFERENCE_BLOCK = 0xFB;\n  private static final int ID_TRACKS = 0x1654AE6B;\n  private static final int ID_TRACK_ENTRY = 0xAE;\n  private static final int ID_TRACK_NUMBER = 0xD7;\n  private static final int ID_TRACK_TYPE = 0x83;\n  private static final int ID_FLAG_DEFAULT = 0x88;\n  private static final int ID_FLAG_FORCED = 0x55AA;\n  private static final int ID_DEFAULT_DURATION = 0x23E383;\n  private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE;\n  private static final int ID_NAME = 0x536E;\n  private static final int ID_CODEC_ID = 0x86;\n  private static final int ID_CODEC_PRIVATE = 0x63A2;\n  private static final int ID_CODEC_DELAY = 0x56AA;\n  private static final int ID_SEEK_PRE_ROLL = 0x56BB;\n  private static final int ID_VIDEO = 0xE0;\n  private static final int ID_PIXEL_WIDTH = 0xB0;\n  private static final int ID_PIXEL_HEIGHT = 0xBA;\n  private static final int ID_DISPLAY_WIDTH = 0x54B0;\n  private static final int ID_DISPLAY_HEIGHT = 0x54BA;\n  private static final int ID_DISPLAY_UNIT = 0x54B2;\n  private static final int ID_AUDIO = 0xE1;\n  private static final int ID_CHANNELS = 0x9F;\n  private static final int ID_AUDIO_BIT_DEPTH = 0x6264;\n  private static final int ID_SAMPLING_FREQUENCY = 0xB5;\n  private static final int ID_CONTENT_ENCODINGS = 0x6D80;\n  private static final int ID_CONTENT_ENCODING = 0x6240;\n  private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;\n  private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;\n  private static final int ID_CONTENT_COMPRESSION = 0x5034;\n  private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254;\n  private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255;\n  private static final int ID_CONTENT_ENCRYPTION = 0x5035;\n  private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;\n  private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;\n  private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;\n  private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;\n  private static final int ID_CUES = 0x1C53BB6B;\n  private static final int ID_CUE_POINT = 0xBB;\n  private static final int ID_CUE_TIME = 0xB3;\n  private static final int ID_CUE_TRACK_POSITIONS = 0xB7;\n  private static final int ID_CUE_CLUSTER_POSITION = 0xF1;\n  private static final int ID_LANGUAGE = 0x22B59C;\n  private static final int ID_PROJECTION = 0x7670;\n  private static final int ID_PROJECTION_TYPE = 0x7671;\n  private static final int ID_PROJECTION_PRIVATE = 0x7672;\n  private static final int ID_PROJECTION_POSE_YAW = 0x7673;\n  private static final int ID_PROJECTION_POSE_PITCH = 0x7674;\n  private static final int ID_PROJECTION_POSE_ROLL = 0x7675;\n  private static final int ID_STEREO_MODE = 0x53B8;\n  private static final int ID_COLOUR = 0x55B0;\n  private static final int ID_COLOUR_RANGE = 0x55B9;\n  private static final int ID_COLOUR_TRANSFER = 0x55BA;\n  private static final int ID_COLOUR_PRIMARIES = 0x55BB;\n  private static final int ID_MAX_CLL = 0x55BC;\n  private static final int ID_MAX_FALL = 0x55BD;\n  private static final int ID_MASTERING_METADATA = 0x55D0;\n  private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1;\n  private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2;\n  private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3;\n  private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4;\n  private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5;\n  private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6;\n  private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7;\n  private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8;\n  private static final int ID_LUMNINANCE_MAX = 0x55D9;\n  private static final int ID_LUMNINANCE_MIN = 0x55DA;\n\n  /**\n   * BlockAddID value for ITU T.35 metadata in a VP9 track. See also\n   * https://www.webmproject.org/docs/container/.\n   */\n  private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4;\n\n  private static final int LACING_NONE = 0;\n  private static final int LACING_XIPH = 1;\n  private static final int LACING_FIXED_SIZE = 2;\n  private static final int LACING_EBML = 3;\n\n  private static final int FOURCC_COMPRESSION_DIVX = 0x58564944;\n  private static final int FOURCC_COMPRESSION_H263 = 0x33363248;\n  private static final int FOURCC_COMPRESSION_VC1 = 0x31435657;\n\n  /**\n   * A template for the prefix that must be added to each subrip sample.\n   *\n   * <p>The display time of each subtitle is passed as {@code timeUs} to {@link\n   * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to\n   * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at\n   * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with\n   * the duration of the subtitle.\n   *\n   * <p>Equivalent to the UTF-8 string: \"1\\n00:00:00,000 --> 00:00:00,000\\n\".\n   */\n  private static final byte[] SUBRIP_PREFIX =\n      new byte[] {\n        49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48,\n        48, 58, 48, 48, 44, 48, 48, 48, 10\n      };\n  /**\n   * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.\n   */\n  private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;\n  /**\n   * The value by which to divide a time in microseconds to convert it to the unit of the last value\n   * in a subrip timecode (milliseconds).\n   */\n  private static final long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000;\n  /**\n   * The format of a subrip timecode.\n   */\n  private static final String SUBRIP_TIMECODE_FORMAT = \"%02d:%02d:%02d,%03d\";\n\n  /**\n   * Matroska specific format line for SSA subtitles.\n   */\n  private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes(\"Format: Start, End, \"\n      + \"ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text\");\n  /**\n   * A template for the prefix that must be added to each SSA sample.\n   *\n   * <p>The display time of each subtitle is passed as {@code timeUs} to {@link\n   * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to\n   * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at\n   * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with\n   * the duration of the subtitle.\n   *\n   * <p>Equivalent to the UTF-8 string: \"Dialogue: 0:00:00:00,0:00:00:00,\".\n   */\n  private static final byte[] SSA_PREFIX =\n      new byte[] {\n        68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44,\n        48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44\n      };\n  /**\n   * The byte offset of the end timecode in {@link #SSA_PREFIX}.\n   */\n  private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21;\n  /**\n   * The value by which to divide a time in microseconds to convert it to the unit of the last value\n   * in an SSA timecode (1/100ths of a second).\n   */\n  private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000;\n  /**\n   * The format of an SSA timecode.\n   */\n  private static final String SSA_TIMECODE_FORMAT = \"%01d:%02d:%02d:%02d\";\n\n  /**\n   * The length in bytes of a WAVEFORMATEX structure.\n   */\n  private static final int WAVE_FORMAT_SIZE = 18;\n  /**\n   * Format tag indicating a WAVEFORMATEXTENSIBLE structure.\n   */\n  private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE;\n  /**\n   * Format tag for PCM.\n   */\n  private static final int WAVE_FORMAT_PCM = 1;\n  /**\n   * Sub format for PCM.\n   */\n  private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L);\n\n  private final EbmlReader reader;\n  private final VarintReader varintReader;\n  private final SparseArray<Track> tracks;\n  private final boolean seekForCuesEnabled;\n\n  // Temporary arrays.\n  private final ParsableByteArray nalStartCode;\n  private final ParsableByteArray nalLength;\n  private final ParsableByteArray scratch;\n  private final ParsableByteArray vorbisNumPageSamples;\n  private final ParsableByteArray seekEntryIdBytes;\n  private final ParsableByteArray sampleStrippedBytes;\n  private final ParsableByteArray subtitleSample;\n  private final ParsableByteArray encryptionInitializationVector;\n  private final ParsableByteArray encryptionSubsampleData;\n  private final ParsableByteArray blockAdditionalData;\n  private ByteBuffer encryptionSubsampleDataBuffer;\n\n  private long segmentContentSize;\n  private long segmentContentPosition = C.POSITION_UNSET;\n  private long timecodeScale = C.TIME_UNSET;\n  private long durationTimecode = C.TIME_UNSET;\n  private long durationUs = C.TIME_UNSET;\n\n  // The track corresponding to the current TrackEntry element, or null.\n  private Track currentTrack;\n\n  // Whether a seek map has been sent to the output.\n  private boolean sentSeekMap;\n\n  // Master seek entry related elements.\n  private int seekEntryId;\n  private long seekEntryPosition;\n\n  // Cue related elements.\n  private boolean seekForCues;\n  private long cuesContentPosition = C.POSITION_UNSET;\n  private long seekPositionAfterBuildingCues = C.POSITION_UNSET;\n  private long clusterTimecodeUs = C.TIME_UNSET;\n  private LongArray cueTimesUs;\n  private LongArray cueClusterPositions;\n  private boolean seenClusterPositionForCurrentCuePoint;\n\n  // Reading state.\n  private boolean haveOutputSample;\n\n  // Block reading state.\n  private int blockState;\n  private long blockTimeUs;\n  private long blockDurationUs;\n  private int blockSampleIndex;\n  private int blockSampleCount;\n  private int[] blockSampleSizes;\n  private int blockTrackNumber;\n  private int blockTrackNumberLength;\n  @C.BufferFlags\n  private int blockFlags;\n  private int blockAdditionalId;\n  private boolean blockHasReferenceBlock;\n\n  // Sample writing state.\n  private int sampleBytesRead;\n  private int sampleBytesWritten;\n  private int sampleCurrentNalBytesRemaining;\n  private boolean sampleEncodingHandled;\n  private boolean sampleSignalByteRead;\n  private boolean samplePartitionCountRead;\n  private int samplePartitionCount;\n  private byte sampleSignalByte;\n  private boolean sampleInitializationVectorRead;\n\n  // Extractor outputs.\n  private ExtractorOutput extractorOutput;\n\n  public MatroskaExtractor() {\n    this(0);\n  }\n\n  public MatroskaExtractor(@Flags int flags) {\n    this(new DefaultEbmlReader(), flags);\n  }\n\n  /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) {\n    this.reader = reader;\n    this.reader.init(new InnerEbmlProcessor());\n    seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0;\n    varintReader = new VarintReader();\n    tracks = new SparseArray<>();\n    scratch = new ParsableByteArray(4);\n    vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());\n    seekEntryIdBytes = new ParsableByteArray(4);\n    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);\n    nalLength = new ParsableByteArray(4);\n    sampleStrippedBytes = new ParsableByteArray();\n    subtitleSample = new ParsableByteArray();\n    encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);\n    encryptionSubsampleData = new ParsableByteArray();\n    blockAdditionalData = new ParsableByteArray();\n  }\n\n  @Override\n  public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return new Sniffer().sniff(input);\n  }\n\n  @Override\n  public final void init(ExtractorOutput output) {\n    extractorOutput = output;\n  }\n\n  @CallSuper\n  @Override\n  public void seek(long position, long timeUs) {\n    clusterTimecodeUs = C.TIME_UNSET;\n    blockState = BLOCK_STATE_START;\n    reader.reset();\n    varintReader.reset();\n    resetWriteSampleData();\n    for (int i = 0; i < tracks.size(); i++) {\n      tracks.valueAt(i).reset();\n    }\n  }\n\n  @Override\n  public final void release() {\n    // Do nothing\n  }\n\n  @Override\n  public final int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    haveOutputSample = false;\n    boolean continueReading = true;\n    while (continueReading && !haveOutputSample) {\n      continueReading = reader.read(input);\n      if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) {\n        return Extractor.RESULT_SEEK;\n      }\n    }\n    if (!continueReading) {\n      for (int i = 0; i < tracks.size(); i++) {\n        tracks.valueAt(i).outputPendingSampleMetadata();\n      }\n      return Extractor.RESULT_END_OF_INPUT;\n    }\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  /**\n   * Maps an element ID to a corresponding type.\n   *\n   * @see EbmlProcessor#getElementType(int)\n   */\n  @CallSuper\n  @EbmlProcessor.ElementType\n  protected int getElementType(int id) {\n    switch (id) {\n      case ID_EBML:\n      case ID_SEGMENT:\n      case ID_SEEK_HEAD:\n      case ID_SEEK:\n      case ID_INFO:\n      case ID_CLUSTER:\n      case ID_TRACKS:\n      case ID_TRACK_ENTRY:\n      case ID_AUDIO:\n      case ID_VIDEO:\n      case ID_CONTENT_ENCODINGS:\n      case ID_CONTENT_ENCODING:\n      case ID_CONTENT_COMPRESSION:\n      case ID_CONTENT_ENCRYPTION:\n      case ID_CONTENT_ENCRYPTION_AES_SETTINGS:\n      case ID_CUES:\n      case ID_CUE_POINT:\n      case ID_CUE_TRACK_POSITIONS:\n      case ID_BLOCK_GROUP:\n      case ID_BLOCK_ADDITIONS:\n      case ID_BLOCK_MORE:\n      case ID_PROJECTION:\n      case ID_COLOUR:\n      case ID_MASTERING_METADATA:\n        return EbmlProcessor.ELEMENT_TYPE_MASTER;\n      case ID_EBML_READ_VERSION:\n      case ID_DOC_TYPE_READ_VERSION:\n      case ID_SEEK_POSITION:\n      case ID_TIMECODE_SCALE:\n      case ID_TIME_CODE:\n      case ID_BLOCK_DURATION:\n      case ID_PIXEL_WIDTH:\n      case ID_PIXEL_HEIGHT:\n      case ID_DISPLAY_WIDTH:\n      case ID_DISPLAY_HEIGHT:\n      case ID_DISPLAY_UNIT:\n      case ID_TRACK_NUMBER:\n      case ID_TRACK_TYPE:\n      case ID_FLAG_DEFAULT:\n      case ID_FLAG_FORCED:\n      case ID_DEFAULT_DURATION:\n      case ID_MAX_BLOCK_ADDITION_ID:\n      case ID_CODEC_DELAY:\n      case ID_SEEK_PRE_ROLL:\n      case ID_CHANNELS:\n      case ID_AUDIO_BIT_DEPTH:\n      case ID_CONTENT_ENCODING_ORDER:\n      case ID_CONTENT_ENCODING_SCOPE:\n      case ID_CONTENT_COMPRESSION_ALGORITHM:\n      case ID_CONTENT_ENCRYPTION_ALGORITHM:\n      case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:\n      case ID_CUE_TIME:\n      case ID_CUE_CLUSTER_POSITION:\n      case ID_REFERENCE_BLOCK:\n      case ID_STEREO_MODE:\n      case ID_COLOUR_RANGE:\n      case ID_COLOUR_TRANSFER:\n      case ID_COLOUR_PRIMARIES:\n      case ID_MAX_CLL:\n      case ID_MAX_FALL:\n      case ID_PROJECTION_TYPE:\n      case ID_BLOCK_ADD_ID:\n        return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT;\n      case ID_DOC_TYPE:\n      case ID_NAME:\n      case ID_CODEC_ID:\n      case ID_LANGUAGE:\n        return EbmlProcessor.ELEMENT_TYPE_STRING;\n      case ID_SEEK_ID:\n      case ID_CONTENT_COMPRESSION_SETTINGS:\n      case ID_CONTENT_ENCRYPTION_KEY_ID:\n      case ID_SIMPLE_BLOCK:\n      case ID_BLOCK:\n      case ID_CODEC_PRIVATE:\n      case ID_PROJECTION_PRIVATE:\n      case ID_BLOCK_ADDITIONAL:\n        return EbmlProcessor.ELEMENT_TYPE_BINARY;\n      case ID_DURATION:\n      case ID_SAMPLING_FREQUENCY:\n      case ID_PRIMARY_R_CHROMATICITY_X:\n      case ID_PRIMARY_R_CHROMATICITY_Y:\n      case ID_PRIMARY_G_CHROMATICITY_X:\n      case ID_PRIMARY_G_CHROMATICITY_Y:\n      case ID_PRIMARY_B_CHROMATICITY_X:\n      case ID_PRIMARY_B_CHROMATICITY_Y:\n      case ID_WHITE_POINT_CHROMATICITY_X:\n      case ID_WHITE_POINT_CHROMATICITY_Y:\n      case ID_LUMNINANCE_MAX:\n      case ID_LUMNINANCE_MIN:\n      case ID_PROJECTION_POSE_YAW:\n      case ID_PROJECTION_POSE_PITCH:\n      case ID_PROJECTION_POSE_ROLL:\n        return EbmlProcessor.ELEMENT_TYPE_FLOAT;\n      default:\n        return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;\n    }\n  }\n\n  /**\n   * Checks if the given id is that of a level 1 element.\n   *\n   * @see EbmlProcessor#isLevel1Element(int)\n   */\n  @CallSuper\n  protected boolean isLevel1Element(int id) {\n    return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;\n  }\n\n  /**\n   * Called when the start of a master element is encountered.\n   *\n   * @see EbmlProcessor#startMasterElement(int, long, long)\n   */\n  @CallSuper\n  protected void startMasterElement(int id, long contentPosition, long contentSize)\n      throws ParserException {\n    switch (id) {\n      case ID_SEGMENT:\n        if (segmentContentPosition != C.POSITION_UNSET\n            && segmentContentPosition != contentPosition) {\n          throw new ParserException(\"Multiple Segment elements not supported\");\n        }\n        segmentContentPosition = contentPosition;\n        segmentContentSize = contentSize;\n        break;\n      case ID_SEEK:\n        seekEntryId = UNSET_ENTRY_ID;\n        seekEntryPosition = C.POSITION_UNSET;\n        break;\n      case ID_CUES:\n        cueTimesUs = new LongArray();\n        cueClusterPositions = new LongArray();\n        break;\n      case ID_CUE_POINT:\n        seenClusterPositionForCurrentCuePoint = false;\n        break;\n      case ID_CLUSTER:\n        if (!sentSeekMap) {\n          // We need to build cues before parsing the cluster.\n          if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) {\n            // We know where the Cues element is located. Seek to request it.\n            seekForCues = true;\n          } else {\n            // We don't know where the Cues element is located. It's most likely omitted. Allow\n            // playback, but disable seeking.\n            extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));\n            sentSeekMap = true;\n          }\n        }\n        break;\n      case ID_BLOCK_GROUP:\n        blockHasReferenceBlock = false;\n        break;\n      case ID_CONTENT_ENCODING:\n        // TODO: check and fail if more than one content encoding is present.\n        break;\n      case ID_CONTENT_ENCRYPTION:\n        currentTrack.hasContentEncryption = true;\n        break;\n      case ID_TRACK_ENTRY:\n        currentTrack = new Track();\n        break;\n      case ID_MASTERING_METADATA:\n        currentTrack.hasColorInfo = true;\n        break;\n      default:\n        break;\n    }\n  }\n\n  /**\n   * Called when the end of a master element is encountered.\n   *\n   * @see EbmlProcessor#endMasterElement(int)\n   */\n  @CallSuper\n  protected void endMasterElement(int id) throws ParserException {\n    switch (id) {\n      case ID_SEGMENT_INFO:\n        if (timecodeScale == C.TIME_UNSET) {\n          // timecodeScale was omitted. Use the default value.\n          timecodeScale = 1000000;\n        }\n        if (durationTimecode != C.TIME_UNSET) {\n          durationUs = scaleTimecodeToUs(durationTimecode);\n        }\n        break;\n      case ID_SEEK:\n        if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) {\n          throw new ParserException(\"Mandatory element SeekID or SeekPosition not found\");\n        }\n        if (seekEntryId == ID_CUES) {\n          cuesContentPosition = seekEntryPosition;\n        }\n        break;\n      case ID_CUES:\n        if (!sentSeekMap) {\n          extractorOutput.seekMap(buildSeekMap());\n          sentSeekMap = true;\n        } else {\n          // We have already built the cues. Ignore.\n        }\n        break;\n      case ID_BLOCK_GROUP:\n        if (blockState != BLOCK_STATE_DATA) {\n          // We've skipped this block (due to incompatible track number).\n          return;\n        }\n        // Commit sample metadata.\n        int sampleOffset = 0;\n        for (int i = 0; i < blockSampleCount; i++) {\n          sampleOffset += blockSampleSizes[i];\n        }\n        Track track = tracks.get(blockTrackNumber);\n        for (int i = 0; i < blockSampleCount; i++) {\n          long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000;\n          int sampleFlags = blockFlags;\n          if (i == 0 && !blockHasReferenceBlock) {\n            // If the ReferenceBlock element was not found in this block, then the first frame is a\n            // keyframe.\n            sampleFlags |= C.BUFFER_FLAG_KEY_FRAME;\n          }\n          int sampleSize = blockSampleSizes[i];\n          sampleOffset -= sampleSize; // The offset is to the end of the sample.\n          commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset);\n        }\n        blockState = BLOCK_STATE_START;\n        break;\n      case ID_CONTENT_ENCODING:\n        if (currentTrack.hasContentEncryption) {\n          if (currentTrack.cryptoData == null) {\n            throw new ParserException(\"Encrypted Track found but ContentEncKeyID was not found\");\n          }\n          currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL,\n              MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey));\n        }\n        break;\n      case ID_CONTENT_ENCODINGS:\n        if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) {\n          throw new ParserException(\"Combining encryption and compression is not supported\");\n        }\n        break;\n      case ID_TRACK_ENTRY:\n        if (isCodecSupported(currentTrack.codecId)) {\n          currentTrack.initializeOutput(extractorOutput, currentTrack.number);\n          tracks.put(currentTrack.number, currentTrack);\n        }\n        currentTrack = null;\n        break;\n      case ID_TRACKS:\n        if (tracks.size() == 0) {\n          throw new ParserException(\"No valid tracks were found\");\n        }\n        extractorOutput.endTracks();\n        break;\n      default:\n        break;\n    }\n  }\n\n  /**\n   * Called when an integer element is encountered.\n   *\n   * @see EbmlProcessor#integerElement(int, long)\n   */\n  @CallSuper\n  protected void integerElement(int id, long value) throws ParserException {\n    switch (id) {\n      case ID_EBML_READ_VERSION:\n        // Validate that EBMLReadVersion is supported. This extractor only supports v1.\n        if (value != 1) {\n          throw new ParserException(\"EBMLReadVersion \" + value + \" not supported\");\n        }\n        break;\n      case ID_DOC_TYPE_READ_VERSION:\n        // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.\n        if (value < 1 || value > 2) {\n          throw new ParserException(\"DocTypeReadVersion \" + value + \" not supported\");\n        }\n        break;\n      case ID_SEEK_POSITION:\n        // Seek Position is the relative offset beginning from the Segment. So to get absolute\n        // offset from the beginning of the file, we need to add segmentContentPosition to it.\n        seekEntryPosition = value + segmentContentPosition;\n        break;\n      case ID_TIMECODE_SCALE:\n        timecodeScale = value;\n        break;\n      case ID_PIXEL_WIDTH:\n        currentTrack.width = (int) value;\n        break;\n      case ID_PIXEL_HEIGHT:\n        currentTrack.height = (int) value;\n        break;\n      case ID_DISPLAY_WIDTH:\n        currentTrack.displayWidth = (int) value;\n        break;\n      case ID_DISPLAY_HEIGHT:\n        currentTrack.displayHeight = (int) value;\n        break;\n      case ID_DISPLAY_UNIT:\n        currentTrack.displayUnit = (int) value;\n        break;\n      case ID_TRACK_NUMBER:\n        currentTrack.number = (int) value;\n        break;\n      case ID_FLAG_DEFAULT:\n        currentTrack.flagDefault = value == 1;\n        break;\n      case ID_FLAG_FORCED:\n        currentTrack.flagForced = value == 1;\n        break;\n      case ID_TRACK_TYPE:\n        currentTrack.type = (int) value;\n        break;\n      case ID_DEFAULT_DURATION:\n        currentTrack.defaultSampleDurationNs = (int) value;\n        break;\n      case ID_MAX_BLOCK_ADDITION_ID:\n        currentTrack.maxBlockAdditionId = (int) value;\n        break;\n      case ID_CODEC_DELAY:\n        currentTrack.codecDelayNs = value;\n        break;\n      case ID_SEEK_PRE_ROLL:\n        currentTrack.seekPreRollNs = value;\n        break;\n      case ID_CHANNELS:\n        currentTrack.channelCount = (int) value;\n        break;\n      case ID_AUDIO_BIT_DEPTH:\n        currentTrack.audioBitDepth = (int) value;\n        break;\n      case ID_REFERENCE_BLOCK:\n        blockHasReferenceBlock = true;\n        break;\n      case ID_CONTENT_ENCODING_ORDER:\n        // This extractor only supports one ContentEncoding element and hence the order has to be 0.\n        if (value != 0) {\n          throw new ParserException(\"ContentEncodingOrder \" + value + \" not supported\");\n        }\n        break;\n      case ID_CONTENT_ENCODING_SCOPE:\n        // This extractor only supports the scope of all frames.\n        if (value != 1) {\n          throw new ParserException(\"ContentEncodingScope \" + value + \" not supported\");\n        }\n        break;\n      case ID_CONTENT_COMPRESSION_ALGORITHM:\n        // This extractor only supports header stripping.\n        if (value != 3) {\n          throw new ParserException(\"ContentCompAlgo \" + value + \" not supported\");\n        }\n        break;\n      case ID_CONTENT_ENCRYPTION_ALGORITHM:\n        // Only the value 5 (AES) is allowed according to the WebM specification.\n        if (value != 5) {\n          throw new ParserException(\"ContentEncAlgo \" + value + \" not supported\");\n        }\n        break;\n      case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:\n        // Only the value 1 is allowed according to the WebM specification.\n        if (value != 1) {\n          throw new ParserException(\"AESSettingsCipherMode \" + value + \" not supported\");\n        }\n        break;\n      case ID_CUE_TIME:\n        cueTimesUs.add(scaleTimecodeToUs(value));\n        break;\n      case ID_CUE_CLUSTER_POSITION:\n        if (!seenClusterPositionForCurrentCuePoint) {\n          // If there's more than one video/audio track, then there could be more than one\n          // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first\n          // one (since the cluster position will be quite close for all the tracks).\n          cueClusterPositions.add(value);\n          seenClusterPositionForCurrentCuePoint = true;\n        }\n        break;\n      case ID_TIME_CODE:\n        clusterTimecodeUs = scaleTimecodeToUs(value);\n        break;\n      case ID_BLOCK_DURATION:\n        blockDurationUs = scaleTimecodeToUs(value);\n        break;\n      case ID_STEREO_MODE:\n        int layout = (int) value;\n        switch (layout) {\n          case 0:\n            currentTrack.stereoMode = C.STEREO_MODE_MONO;\n            break;\n          case 1:\n            currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT;\n            break;\n          case 3:\n            currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;\n            break;\n          case 15:\n            currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;\n            break;\n          default:\n            break;\n        }\n        break;\n      case ID_COLOUR_PRIMARIES:\n        currentTrack.hasColorInfo = true;\n        switch ((int) value) {\n          case 1:\n            currentTrack.colorSpace = C.COLOR_SPACE_BT709;\n            break;\n          case 4:  // BT.470M.\n          case 5:  // BT.470BG.\n          case 6:  // SMPTE 170M.\n          case 7:  // SMPTE 240M.\n            currentTrack.colorSpace = C.COLOR_SPACE_BT601;\n            break;\n          case 9:\n            currentTrack.colorSpace = C.COLOR_SPACE_BT2020;\n            break;\n          default:\n            break;\n        }\n        break;\n      case ID_COLOUR_TRANSFER:\n        switch ((int) value) {\n          case 1:  // BT.709.\n          case 6:  // SMPTE 170M.\n          case 7:  // SMPTE 240M.\n            currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR;\n            break;\n          case 16:\n            currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084;\n            break;\n          case 18:\n            currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG;\n            break;\n          default:\n            break;\n        }\n        break;\n      case ID_COLOUR_RANGE:\n        switch((int) value) {\n          case 1:  // Broadcast range.\n            currentTrack.colorRange = C.COLOR_RANGE_LIMITED;\n            break;\n          case 2:\n            currentTrack.colorRange = C.COLOR_RANGE_FULL;\n            break;\n          default:\n            break;\n        }\n        break;\n      case ID_MAX_CLL:\n        currentTrack.maxContentLuminance = (int) value;\n        break;\n      case ID_MAX_FALL:\n        currentTrack.maxFrameAverageLuminance = (int) value;\n        break;\n      case ID_PROJECTION_TYPE:\n        switch ((int) value) {\n          case 0:\n            currentTrack.projectionType = C.PROJECTION_RECTANGULAR;\n            break;\n          case 1:\n            currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR;\n            break;\n          case 2:\n            currentTrack.projectionType = C.PROJECTION_CUBEMAP;\n            break;\n          case 3:\n            currentTrack.projectionType = C.PROJECTION_MESH;\n            break;\n          default:\n            break;\n        }\n        break;\n      case ID_BLOCK_ADD_ID:\n        blockAdditionalId = (int) value;\n        break;\n      default:\n        break;\n    }\n  }\n\n  /**\n   * Called when a float element is encountered.\n   *\n   * @see EbmlProcessor#floatElement(int, double)\n   */\n  @CallSuper\n  protected void floatElement(int id, double value) throws ParserException {\n    switch (id) {\n      case ID_DURATION:\n        durationTimecode = (long) value;\n        break;\n      case ID_SAMPLING_FREQUENCY:\n        currentTrack.sampleRate = (int) value;\n        break;\n      case ID_PRIMARY_R_CHROMATICITY_X:\n        currentTrack.primaryRChromaticityX = (float) value;\n        break;\n      case ID_PRIMARY_R_CHROMATICITY_Y:\n        currentTrack.primaryRChromaticityY = (float) value;\n        break;\n      case ID_PRIMARY_G_CHROMATICITY_X:\n        currentTrack.primaryGChromaticityX = (float) value;\n        break;\n      case ID_PRIMARY_G_CHROMATICITY_Y:\n        currentTrack.primaryGChromaticityY = (float) value;\n        break;\n      case ID_PRIMARY_B_CHROMATICITY_X:\n        currentTrack.primaryBChromaticityX = (float) value;\n        break;\n      case ID_PRIMARY_B_CHROMATICITY_Y:\n        currentTrack.primaryBChromaticityY = (float) value;\n        break;\n      case ID_WHITE_POINT_CHROMATICITY_X:\n        currentTrack.whitePointChromaticityX = (float) value;\n        break;\n      case ID_WHITE_POINT_CHROMATICITY_Y:\n        currentTrack.whitePointChromaticityY = (float) value;\n        break;\n      case ID_LUMNINANCE_MAX:\n        currentTrack.maxMasteringLuminance = (float) value;\n        break;\n      case ID_LUMNINANCE_MIN:\n        currentTrack.minMasteringLuminance = (float) value;\n        break;\n      case ID_PROJECTION_POSE_YAW:\n        currentTrack.projectionPoseYaw = (float) value;\n        break;\n      case ID_PROJECTION_POSE_PITCH:\n        currentTrack.projectionPosePitch = (float) value;\n        break;\n      case ID_PROJECTION_POSE_ROLL:\n        currentTrack.projectionPoseRoll = (float) value;\n        break;\n      default:\n        break;\n    }\n  }\n\n  /**\n   * Called when a string element is encountered.\n   *\n   * @see EbmlProcessor#stringElement(int, String)\n   */\n  @CallSuper\n  protected void stringElement(int id, String value) throws ParserException {\n    switch (id) {\n      case ID_DOC_TYPE:\n        // Validate that DocType is supported.\n        if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) {\n          throw new ParserException(\"DocType \" + value + \" not supported\");\n        }\n        break;\n      case ID_NAME:\n        currentTrack.name = value;\n        break;\n      case ID_CODEC_ID:\n        currentTrack.codecId = value;\n        break;\n      case ID_LANGUAGE:\n        currentTrack.language = value;\n        break;\n      default:\n        break;\n    }\n  }\n\n  /**\n   * Called when a binary element is encountered.\n   *\n   * @see EbmlProcessor#binaryElement(int, int, ExtractorInput)\n   */\n  @CallSuper\n  protected void binaryElement(int id, int contentSize, ExtractorInput input)\n      throws IOException, InterruptedException {\n    switch (id) {\n      case ID_SEEK_ID:\n        Arrays.fill(seekEntryIdBytes.data, (byte) 0);\n        input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize);\n        seekEntryIdBytes.setPosition(0);\n        seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();\n        break;\n      case ID_CODEC_PRIVATE:\n        currentTrack.codecPrivate = new byte[contentSize];\n        input.readFully(currentTrack.codecPrivate, 0, contentSize);\n        break;\n      case ID_PROJECTION_PRIVATE:\n        currentTrack.projectionData = new byte[contentSize];\n        input.readFully(currentTrack.projectionData, 0, contentSize);\n        break;\n      case ID_CONTENT_COMPRESSION_SETTINGS:\n        // This extractor only supports header stripping, so the payload is the stripped bytes.\n        currentTrack.sampleStrippedBytes = new byte[contentSize];\n        input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);\n        break;\n      case ID_CONTENT_ENCRYPTION_KEY_ID:\n        byte[] encryptionKey = new byte[contentSize];\n        input.readFully(encryptionKey, 0, contentSize);\n        currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey,\n            0, 0); // We assume patternless AES-CTR.\n        break;\n      case ID_SIMPLE_BLOCK:\n      case ID_BLOCK:\n        // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure\n        // and http://matroska.org/technical/specs/index.html#block_structure\n        // for info about how data is organized in SimpleBlock and Block elements respectively. They\n        // differ only in the way flags are specified.\n\n        if (blockState == BLOCK_STATE_START) {\n          blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8);\n          blockTrackNumberLength = varintReader.getLastLength();\n          blockDurationUs = C.TIME_UNSET;\n          blockState = BLOCK_STATE_HEADER;\n          scratch.reset();\n        }\n\n        Track track = tracks.get(blockTrackNumber);\n\n        // Ignore the block if we don't know about the track to which it belongs.\n        if (track == null) {\n          input.skipFully(contentSize - blockTrackNumberLength);\n          blockState = BLOCK_STATE_START;\n          return;\n        }\n\n        if (blockState == BLOCK_STATE_HEADER) {\n          // Read the relative timecode (2 bytes) and flags (1 byte).\n          readScratch(input, 3);\n          int lacing = (scratch.data[2] & 0x06) >> 1;\n          if (lacing == LACING_NONE) {\n            blockSampleCount = 1;\n            blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1);\n            blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3;\n          } else {\n            // Read the sample count (1 byte).\n            readScratch(input, 4);\n            blockSampleCount = (scratch.data[3] & 0xFF) + 1;\n            blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount);\n            if (lacing == LACING_FIXED_SIZE) {\n              int blockLacingSampleSize =\n                  (contentSize - blockTrackNumberLength - 4) / blockSampleCount;\n              Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize);\n            } else if (lacing == LACING_XIPH) {\n              int totalSamplesSize = 0;\n              int headerSize = 4;\n              for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) {\n                blockSampleSizes[sampleIndex] = 0;\n                int byteValue;\n                do {\n                  readScratch(input, ++headerSize);\n                  byteValue = scratch.data[headerSize - 1] & 0xFF;\n                  blockSampleSizes[sampleIndex] += byteValue;\n                } while (byteValue == 0xFF);\n                totalSamplesSize += blockSampleSizes[sampleIndex];\n              }\n              blockSampleSizes[blockSampleCount - 1] =\n                  contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;\n            } else if (lacing == LACING_EBML) {\n              int totalSamplesSize = 0;\n              int headerSize = 4;\n              for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) {\n                blockSampleSizes[sampleIndex] = 0;\n                readScratch(input, ++headerSize);\n                if (scratch.data[headerSize - 1] == 0) {\n                  throw new ParserException(\"No valid varint length mask found\");\n                }\n                long readValue = 0;\n                for (int i = 0; i < 8; i++) {\n                  int lengthMask = 1 << (7 - i);\n                  if ((scratch.data[headerSize - 1] & lengthMask) != 0) {\n                    int readPosition = headerSize - 1;\n                    headerSize += i;\n                    readScratch(input, headerSize);\n                    readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask;\n                    while (readPosition < headerSize) {\n                      readValue <<= 8;\n                      readValue |= (scratch.data[readPosition++] & 0xFF);\n                    }\n                    // The first read value is the first size. Later values are signed offsets.\n                    if (sampleIndex > 0) {\n                      readValue -= (1L << (6 + i * 7)) - 1;\n                    }\n                    break;\n                  }\n                }\n                if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) {\n                  throw new ParserException(\"EBML lacing sample size out of range.\");\n                }\n                int intReadValue = (int) readValue;\n                blockSampleSizes[sampleIndex] =\n                    sampleIndex == 0\n                        ? intReadValue\n                        : blockSampleSizes[sampleIndex - 1] + intReadValue;\n                totalSamplesSize += blockSampleSizes[sampleIndex];\n              }\n              blockSampleSizes[blockSampleCount - 1] =\n                  contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;\n            } else {\n              // Lacing is always in the range 0--3.\n              throw new ParserException(\"Unexpected lacing value: \" + lacing);\n            }\n          }\n\n          int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);\n          blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);\n          boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;\n          boolean isKeyframe = track.type == TRACK_TYPE_AUDIO\n              || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);\n          blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0)\n              | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0);\n          blockState = BLOCK_STATE_DATA;\n          blockSampleIndex = 0;\n        }\n\n        if (id == ID_SIMPLE_BLOCK) {\n          // For SimpleBlock, we can write sample data and immediately commit the corresponding\n          // sample metadata.\n          while (blockSampleIndex < blockSampleCount) {\n            int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]);\n            long sampleTimeUs =\n                blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000;\n            commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0);\n            blockSampleIndex++;\n          }\n          blockState = BLOCK_STATE_START;\n        } else {\n          // For Block, we need to wait until the end of the BlockGroup element before committing\n          // sample metadata. This is so that we can handle ReferenceBlock (which can be used to\n          // infer whether the first sample in the block is a keyframe), and BlockAdditions (which\n          // can contain additional sample data to append) contained in the block group. Just output\n          // the sample data, storing the final sample sizes for when we commit the metadata.\n          while (blockSampleIndex < blockSampleCount) {\n            blockSampleSizes[blockSampleIndex] =\n                writeSampleData(input, track, blockSampleSizes[blockSampleIndex]);\n            blockSampleIndex++;\n          }\n        }\n\n        break;\n      case ID_BLOCK_ADDITIONAL:\n        if (blockState != BLOCK_STATE_DATA) {\n          return;\n        }\n        handleBlockAdditionalData(\n            tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize);\n        break;\n      default:\n        throw new ParserException(\"Unexpected id: \" + id);\n    }\n  }\n\n  protected void handleBlockAdditionalData(\n      Track track, int blockAdditionalId, ExtractorInput input, int contentSize)\n      throws IOException, InterruptedException {\n    if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35\n        && CODEC_ID_VP9.equals(track.codecId)) {\n      blockAdditionalData.reset(contentSize);\n      input.readFully(blockAdditionalData.data, 0, contentSize);\n    } else {\n      // Unhandled block additional data.\n      input.skipFully(contentSize);\n    }\n  }\n\n  private void commitSampleToOutput(\n      Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) {\n    if (track.trueHdSampleRechunker != null) {\n      track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset);\n    } else {\n      if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {\n        if (blockSampleCount > 1) {\n          Log.w(TAG, \"Skipping subtitle sample in laced block.\");\n        } else if (durationUs == C.TIME_UNSET) {\n          Log.w(TAG, \"Skipping subtitle sample with no duration.\");\n        } else {\n          setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data);\n          // Note: If we ever want to support DRM protected subtitles then we'll need to output the\n          // appropriate encryption data here.\n          track.output.sampleData(subtitleSample, subtitleSample.limit());\n          size += subtitleSample.limit();\n        }\n      }\n\n      if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) {\n        if (blockSampleCount > 1) {\n          // There were multiple samples in the block. Appending the additional data to the last\n          // sample doesn't make sense. Skip instead.\n          flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA;\n        } else {\n          // Append supplemental data.\n          int blockAdditionalSize = blockAdditionalData.limit();\n          track.output.sampleData(blockAdditionalData, blockAdditionalSize);\n          size += blockAdditionalSize;\n        }\n      }\n      track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData);\n    }\n    haveOutputSample = true;\n  }\n\n  /**\n   * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from\n   * the extractor input if necessary.\n   */\n  private void readScratch(ExtractorInput input, int requiredLength)\n      throws IOException, InterruptedException {\n    if (scratch.limit() >= requiredLength) {\n      return;\n    }\n    if (scratch.capacity() < requiredLength) {\n      scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)),\n          scratch.limit());\n    }\n    input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());\n    scratch.setLimit(requiredLength);\n  }\n\n  /**\n   * Writes data for a single sample to the track output.\n   *\n   * @param input The input from which to read sample data.\n   * @param track The track to output the sample to.\n   * @param size The size of the sample data on the input side.\n   * @return The final size of the written sample.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private int writeSampleData(ExtractorInput input, Track track, int size)\n      throws IOException, InterruptedException {\n    if (CODEC_ID_SUBRIP.equals(track.codecId)) {\n      writeSubtitleSampleData(input, SUBRIP_PREFIX, size);\n      return finishWriteSampleData();\n    } else if (CODEC_ID_ASS.equals(track.codecId)) {\n      writeSubtitleSampleData(input, SSA_PREFIX, size);\n      return finishWriteSampleData();\n    }\n\n    TrackOutput output = track.output;\n    if (!sampleEncodingHandled) {\n      if (track.hasContentEncryption) {\n        // If the sample is encrypted, read its encryption signal byte and set the IV size.\n        // Clear the encrypted flag.\n        blockFlags &= ~C.BUFFER_FLAG_ENCRYPTED;\n        if (!sampleSignalByteRead) {\n          input.readFully(scratch.data, 0, 1);\n          sampleBytesRead++;\n          if ((scratch.data[0] & 0x80) == 0x80) {\n            throw new ParserException(\"Extension bit is set in signal byte\");\n          }\n          sampleSignalByte = scratch.data[0];\n          sampleSignalByteRead = true;\n        }\n        boolean isEncrypted = (sampleSignalByte & 0x01) == 0x01;\n        if (isEncrypted) {\n          boolean hasSubsampleEncryption = (sampleSignalByte & 0x02) == 0x02;\n          blockFlags |= C.BUFFER_FLAG_ENCRYPTED;\n          if (!sampleInitializationVectorRead) {\n            input.readFully(encryptionInitializationVector.data, 0, ENCRYPTION_IV_SIZE);\n            sampleBytesRead += ENCRYPTION_IV_SIZE;\n            sampleInitializationVectorRead = true;\n            // Write the signal byte, containing the IV size and the subsample encryption flag.\n            scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00));\n            scratch.setPosition(0);\n            output.sampleData(scratch, 1);\n            sampleBytesWritten++;\n            // Write the IV.\n            encryptionInitializationVector.setPosition(0);\n            output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE);\n            sampleBytesWritten += ENCRYPTION_IV_SIZE;\n          }\n          if (hasSubsampleEncryption) {\n            if (!samplePartitionCountRead) {\n              input.readFully(scratch.data, 0, 1);\n              sampleBytesRead++;\n              scratch.setPosition(0);\n              samplePartitionCount = scratch.readUnsignedByte();\n              samplePartitionCountRead = true;\n            }\n            int samplePartitionDataSize = samplePartitionCount * 4;\n            scratch.reset(samplePartitionDataSize);\n            input.readFully(scratch.data, 0, samplePartitionDataSize);\n            sampleBytesRead += samplePartitionDataSize;\n            short subsampleCount = (short) (1 + (samplePartitionCount / 2));\n            int subsampleDataSize = 2 + 6 * subsampleCount;\n            if (encryptionSubsampleDataBuffer == null\n                || encryptionSubsampleDataBuffer.capacity() < subsampleDataSize) {\n              encryptionSubsampleDataBuffer = ByteBuffer.allocate(subsampleDataSize);\n            }\n            encryptionSubsampleDataBuffer.position(0);\n            encryptionSubsampleDataBuffer.putShort(subsampleCount);\n            // Loop through the partition offsets and write out the data in the way ExoPlayer\n            // wants it (ISO 23001-7 Part 7):\n            //   2 bytes - sub sample count.\n            //   for each sub sample:\n            //     2 bytes - clear data size.\n            //     4 bytes - encrypted data size.\n            int partitionOffset = 0;\n            for (int i = 0; i < samplePartitionCount; i++) {\n              int previousPartitionOffset = partitionOffset;\n              partitionOffset = scratch.readUnsignedIntToInt();\n              if ((i % 2) == 0) {\n                encryptionSubsampleDataBuffer.putShort(\n                    (short) (partitionOffset - previousPartitionOffset));\n              } else {\n                encryptionSubsampleDataBuffer.putInt(partitionOffset - previousPartitionOffset);\n              }\n            }\n            int finalPartitionSize = size - sampleBytesRead - partitionOffset;\n            if ((samplePartitionCount % 2) == 1) {\n              encryptionSubsampleDataBuffer.putInt(finalPartitionSize);\n            } else {\n              encryptionSubsampleDataBuffer.putShort((short) finalPartitionSize);\n              encryptionSubsampleDataBuffer.putInt(0);\n            }\n            encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize);\n            output.sampleData(encryptionSubsampleData, subsampleDataSize);\n            sampleBytesWritten += subsampleDataSize;\n          }\n        }\n      } else if (track.sampleStrippedBytes != null) {\n        // If the sample has header stripping, prepare to read/output the stripped bytes first.\n        sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length);\n      }\n\n      if (track.maxBlockAdditionId > 0) {\n        blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA;\n        blockAdditionalData.reset();\n        // If there is supplemental data, the structure of the sample data is:\n        // sample size (4 bytes) || sample data || supplemental data\n        scratch.reset(/* limit= */ 4);\n        scratch.data[0] = (byte) ((size >> 24) & 0xFF);\n        scratch.data[1] = (byte) ((size >> 16) & 0xFF);\n        scratch.data[2] = (byte) ((size >> 8) & 0xFF);\n        scratch.data[3] = (byte) (size & 0xFF);\n        output.sampleData(scratch, 4);\n        sampleBytesWritten += 4;\n      }\n\n      sampleEncodingHandled = true;\n    }\n    size += sampleStrippedBytes.limit();\n\n    if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) {\n      // TODO: Deduplicate with Mp4Extractor.\n\n      // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case\n      // they're only 1 or 2 bytes long.\n      byte[] nalLengthData = nalLength.data;\n      nalLengthData[0] = 0;\n      nalLengthData[1] = 0;\n      nalLengthData[2] = 0;\n      int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;\n      int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;\n      // NAL units are length delimited, but the decoder requires start code delimited units.\n      // Loop until we've written the sample to the track output, replacing length delimiters with\n      // start codes as we encounter them.\n      while (sampleBytesRead < size) {\n        if (sampleCurrentNalBytesRemaining == 0) {\n          // Read the NAL length so that we know where we find the next one.\n          writeToTarget(\n              input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);\n          sampleBytesRead += nalUnitLengthFieldLength;\n          nalLength.setPosition(0);\n          sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();\n          // Write a start code for the current NAL unit.\n          nalStartCode.setPosition(0);\n          output.sampleData(nalStartCode, 4);\n          sampleBytesWritten += 4;\n        } else {\n          // Write the payload of the NAL unit.\n          int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining);\n          sampleBytesRead += bytesWritten;\n          sampleBytesWritten += bytesWritten;\n          sampleCurrentNalBytesRemaining -= bytesWritten;\n        }\n      }\n    } else {\n      if (track.trueHdSampleRechunker != null) {\n        Assertions.checkState(sampleStrippedBytes.limit() == 0);\n        track.trueHdSampleRechunker.startSample(input);\n      }\n      while (sampleBytesRead < size) {\n        int bytesWritten = writeToOutput(input, output, size - sampleBytesRead);\n        sampleBytesRead += bytesWritten;\n        sampleBytesWritten += bytesWritten;\n      }\n    }\n\n    if (CODEC_ID_VORBIS.equals(track.codecId)) {\n      // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the\n      // number of samples in the current page. This definition holds good only for Ogg and\n      // irrelevant for Matroska. So we always set this to -1 (the decoder will ignore this value if\n      // we set it to -1). The android platform media extractor [2] does the same.\n      // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314\n      // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474\n      vorbisNumPageSamples.setPosition(0);\n      output.sampleData(vorbisNumPageSamples, 4);\n      sampleBytesWritten += 4;\n    }\n\n    return finishWriteSampleData();\n  }\n\n  /**\n   * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been\n   * written. Returns the final sample size and resets state for the next sample.\n   */\n  private int finishWriteSampleData() {\n    int sampleSize = sampleBytesWritten;\n    resetWriteSampleData();\n    return sampleSize;\n  }\n\n  /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */\n  private void resetWriteSampleData() {\n    sampleBytesRead = 0;\n    sampleBytesWritten = 0;\n    sampleCurrentNalBytesRemaining = 0;\n    sampleEncodingHandled = false;\n    sampleSignalByteRead = false;\n    samplePartitionCountRead = false;\n    samplePartitionCount = 0;\n    sampleSignalByte = (byte) 0;\n    sampleInitializationVectorRead = false;\n    sampleStrippedBytes.reset();\n  }\n\n  private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size)\n      throws IOException, InterruptedException {\n    int sizeWithPrefix = samplePrefix.length + size;\n    if (subtitleSample.capacity() < sizeWithPrefix) {\n      // Initialize subripSample to contain the required prefix and have space to hold a subtitle\n      // twice as long as this one.\n      subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size);\n    } else {\n      System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length);\n    }\n    input.readFully(subtitleSample.data, samplePrefix.length, size);\n    subtitleSample.reset(sizeWithPrefix);\n    // Defer writing the data to the track output. We need to modify the sample data by setting\n    // the correct end timecode, which we might not have yet.\n  }\n\n  /**\n   * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived\n   * from {@code durationUs}.\n   *\n   * <p>See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use\n   * the duration as the end timecode.\n   *\n   * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}.\n   * @param durationUs The duration of the sample, in microseconds.\n   * @param subtitleData The subtitle sample in which to overwrite the end timecode (output\n   *     parameter).\n   */\n  private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) {\n    byte[] endTimecode;\n    int endTimecodeOffset;\n    switch (codecId) {\n      case CODEC_ID_SUBRIP:\n        endTimecode =\n            formatSubtitleTimecode(\n                durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR);\n        endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET;\n        break;\n      case CODEC_ID_ASS:\n        endTimecode =\n            formatSubtitleTimecode(\n                durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR);\n        endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET;\n        break;\n      default:\n        throw new IllegalArgumentException();\n    }\n    System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length);\n  }\n\n  /**\n   * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code\n   * subtitleSampleData}.\n   */\n  private static byte[] formatSubtitleTimecode(\n      long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) {\n    Assertions.checkArgument(timeUs != C.TIME_UNSET);\n    byte[] timeCodeData;\n    int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND));\n    timeUs -= (hours * 3600 * C.MICROS_PER_SECOND);\n    int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND));\n    timeUs -= (minutes * 60 * C.MICROS_PER_SECOND);\n    int seconds = (int) (timeUs / C.MICROS_PER_SECOND);\n    timeUs -= (seconds * C.MICROS_PER_SECOND);\n    int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor);\n    timeCodeData =\n        Util.getUtf8Bytes(\n            String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue));\n    return timeCodeData;\n  }\n\n  /**\n   * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of\n   * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.\n   */\n  private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length)\n      throws IOException, InterruptedException {\n    int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft());\n    input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes);\n    if (pendingStrippedBytes > 0) {\n      sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes);\n    }\n  }\n\n  /**\n   * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either\n   * {@link #sampleStrippedBytes} or data read from {@code input}.\n   */\n  private int writeToOutput(ExtractorInput input, TrackOutput output, int length)\n      throws IOException, InterruptedException {\n    int bytesWritten;\n    int strippedBytesLeft = sampleStrippedBytes.bytesLeft();\n    if (strippedBytesLeft > 0) {\n      bytesWritten = Math.min(length, strippedBytesLeft);\n      output.sampleData(sampleStrippedBytes, bytesWritten);\n    } else {\n      bytesWritten = output.sampleData(input, length, false);\n    }\n    return bytesWritten;\n  }\n\n  /**\n   * Builds a {@link SeekMap} from the recently gathered Cues information.\n   *\n   * @return The built {@link SeekMap}. The returned {@link SeekMap} may be unseekable if cues\n   *     information was missing or incomplete.\n   */\n  private SeekMap buildSeekMap() {\n    if (segmentContentPosition == C.POSITION_UNSET || durationUs == C.TIME_UNSET\n        || cueTimesUs == null || cueTimesUs.size() == 0\n        || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) {\n      // Cues information is missing or incomplete.\n      cueTimesUs = null;\n      cueClusterPositions = null;\n      return new SeekMap.Unseekable(durationUs);\n    }\n    int cuePointsSize = cueTimesUs.size();\n    int[] sizes = new int[cuePointsSize];\n    long[] offsets = new long[cuePointsSize];\n    long[] durationsUs = new long[cuePointsSize];\n    long[] timesUs = new long[cuePointsSize];\n    for (int i = 0; i < cuePointsSize; i++) {\n      timesUs[i] = cueTimesUs.get(i);\n      offsets[i] = segmentContentPosition + cueClusterPositions.get(i);\n    }\n    for (int i = 0; i < cuePointsSize - 1; i++) {\n      sizes[i] = (int) (offsets[i + 1] - offsets[i]);\n      durationsUs[i] = timesUs[i + 1] - timesUs[i];\n    }\n    sizes[cuePointsSize - 1] =\n        (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]);\n    durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];\n\n    long lastDurationUs = durationsUs[cuePointsSize - 1];\n    if (lastDurationUs <= 0) {\n      Log.w(TAG, \"Discarding last cue point with unexpected duration: \" + lastDurationUs);\n      sizes = Arrays.copyOf(sizes, sizes.length - 1);\n      offsets = Arrays.copyOf(offsets, offsets.length - 1);\n      durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1);\n      timesUs = Arrays.copyOf(timesUs, timesUs.length - 1);\n    }\n\n    cueTimesUs = null;\n    cueClusterPositions = null;\n    return new ChunkIndex(sizes, offsets, durationsUs, timesUs);\n  }\n\n  /**\n   * Updates the position of the holder to Cues element's position if the extractor configuration\n   * permits use of master seek entry. After building Cues sets the holder's position back to where\n   * it was before.\n   *\n   * @param seekPosition The holder whose position will be updated.\n   * @param currentPosition Current position of the input.\n   * @return Whether the seek position was updated.\n   */\n  private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) {\n    if (seekForCues) {\n      seekPositionAfterBuildingCues = currentPosition;\n      seekPosition.position = cuesContentPosition;\n      seekForCues = false;\n      return true;\n    }\n    // After parsing Cues, seek back to original position if available. We will not do this unless\n    // we seeked to get to the Cues in the first place.\n    if (sentSeekMap && seekPositionAfterBuildingCues != C.POSITION_UNSET) {\n      seekPosition.position = seekPositionAfterBuildingCues;\n      seekPositionAfterBuildingCues = C.POSITION_UNSET;\n      return true;\n    }\n    return false;\n  }\n\n  private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException {\n    if (timecodeScale == C.TIME_UNSET) {\n      throw new ParserException(\"Can't scale timecode prior to timecodeScale being set.\");\n    }\n    return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000);\n  }\n\n  private static boolean isCodecSupported(String codecId) {\n    return CODEC_ID_VP8.equals(codecId)\n        || CODEC_ID_VP9.equals(codecId)\n        || CODEC_ID_AV1.equals(codecId)\n        || CODEC_ID_MPEG2.equals(codecId)\n        || CODEC_ID_MPEG4_SP.equals(codecId)\n        || CODEC_ID_MPEG4_ASP.equals(codecId)\n        || CODEC_ID_MPEG4_AP.equals(codecId)\n        || CODEC_ID_H264.equals(codecId)\n        || CODEC_ID_H265.equals(codecId)\n        || CODEC_ID_FOURCC.equals(codecId)\n        || CODEC_ID_THEORA.equals(codecId)\n        || CODEC_ID_OPUS.equals(codecId)\n        || CODEC_ID_VORBIS.equals(codecId)\n        || CODEC_ID_AAC.equals(codecId)\n        || CODEC_ID_MP2.equals(codecId)\n        || CODEC_ID_MP3.equals(codecId)\n        || CODEC_ID_AC3.equals(codecId)\n        || CODEC_ID_E_AC3.equals(codecId)\n        || CODEC_ID_TRUEHD.equals(codecId)\n        || CODEC_ID_DTS.equals(codecId)\n        || CODEC_ID_DTS_EXPRESS.equals(codecId)\n        || CODEC_ID_DTS_LOSSLESS.equals(codecId)\n        || CODEC_ID_FLAC.equals(codecId)\n        || CODEC_ID_ACM.equals(codecId)\n        || CODEC_ID_PCM_INT_LIT.equals(codecId)\n        || CODEC_ID_SUBRIP.equals(codecId)\n        || CODEC_ID_ASS.equals(codecId)\n        || CODEC_ID_VOBSUB.equals(codecId)\n        || CODEC_ID_PGS.equals(codecId)\n        || CODEC_ID_DVBSUB.equals(codecId);\n  }\n\n  /**\n   * Returns an array that can store (at least) {@code length} elements, which will be either a new\n   * array or {@code array} if it's not null and large enough.\n   */\n  private static int[] ensureArrayCapacity(int[] array, int length) {\n    if (array == null) {\n      return new int[length];\n    } else if (array.length >= length) {\n      return array;\n    } else {\n      // Double the size to avoid allocating constantly if the required length increases gradually.\n      return new int[Math.max(array.length * 2, length)];\n    }\n  }\n\n  /** Passes events through to the outer {@link MatroskaExtractor}. */\n  private final class InnerEbmlProcessor implements EbmlProcessor {\n\n    @Override\n    @ElementType\n    public int getElementType(int id) {\n      return MatroskaExtractor.this.getElementType(id);\n    }\n\n    @Override\n    public boolean isLevel1Element(int id) {\n      return MatroskaExtractor.this.isLevel1Element(id);\n    }\n\n    @Override\n    public void startMasterElement(int id, long contentPosition, long contentSize)\n        throws ParserException {\n      MatroskaExtractor.this.startMasterElement(id, contentPosition, contentSize);\n    }\n\n    @Override\n    public void endMasterElement(int id) throws ParserException {\n      MatroskaExtractor.this.endMasterElement(id);\n    }\n\n    @Override\n    public void integerElement(int id, long value) throws ParserException {\n      MatroskaExtractor.this.integerElement(id, value);\n    }\n\n    @Override\n    public void floatElement(int id, double value) throws ParserException {\n      MatroskaExtractor.this.floatElement(id, value);\n    }\n\n    @Override\n    public void stringElement(int id, String value) throws ParserException {\n      MatroskaExtractor.this.stringElement(id, value);\n    }\n\n    @Override\n    public void binaryElement(int id, int contentsSize, ExtractorInput input)\n        throws IOException, InterruptedException {\n      MatroskaExtractor.this.binaryElement(id, contentsSize, input);\n    }\n  }\n\n  /**\n   * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples.\n   */\n  private static final class TrueHdSampleRechunker {\n\n    private final byte[] syncframePrefix;\n\n    private boolean foundSyncframe;\n    private int chunkSampleCount;\n    private long chunkTimeUs;\n    private @C.BufferFlags int chunkFlags;\n    private int chunkSize;\n    private int chunkOffset;\n\n    public TrueHdSampleRechunker() {\n      syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH];\n    }\n\n    public void reset() {\n      foundSyncframe = false;\n      chunkSampleCount = 0;\n    }\n\n    public void startSample(ExtractorInput input) throws IOException, InterruptedException {\n      if (foundSyncframe) {\n        return;\n      }\n      input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH);\n      input.resetPeekPosition();\n      if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) {\n        return;\n      }\n      foundSyncframe = true;\n    }\n\n    public void sampleMetadata(\n        Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) {\n      if (!foundSyncframe) {\n        return;\n      }\n      if (chunkSampleCount++ == 0) {\n        // This is the first sample in the chunk.\n        chunkTimeUs = timeUs;\n        chunkFlags = flags;\n        chunkSize = 0;\n      }\n      chunkSize += size;\n      chunkOffset = offset; // The offset is to the end of the sample.\n      if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {\n        // We haven't read enough samples to output a chunk.\n        return;\n      }\n      outputPendingSampleMetadata(track);\n    }\n\n    public void outputPendingSampleMetadata(Track track) {\n      if (chunkSampleCount > 0) {\n        track.output.sampleMetadata(\n            chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData);\n        chunkSampleCount = 0;\n      }\n    }\n  }\n\n  private static final class Track {\n\n    private static final int DISPLAY_UNIT_PIXELS = 0;\n    private static final int MAX_CHROMATICITY = 50000;  // Defined in CTA-861.3.\n    /**\n     * Default max content light level (CLL) that should be encoded into hdrStaticInfo.\n     */\n    private static final int DEFAULT_MAX_CLL = 1000;  // nits.\n\n    /**\n     * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo.\n     */\n    private static final int DEFAULT_MAX_FALL = 200;  // nits.\n\n    // Common elements.\n    public String name;\n    public String codecId;\n    public int number;\n    public int type;\n    public int defaultSampleDurationNs;\n    public int maxBlockAdditionId;\n    public boolean hasContentEncryption;\n    public byte[] sampleStrippedBytes;\n    public TrackOutput.CryptoData cryptoData;\n    public byte[] codecPrivate;\n    public DrmInitData drmInitData;\n\n    // Video elements.\n    public int width = Format.NO_VALUE;\n    public int height = Format.NO_VALUE;\n    public int displayWidth = Format.NO_VALUE;\n    public int displayHeight = Format.NO_VALUE;\n    public int displayUnit = DISPLAY_UNIT_PIXELS;\n    @C.Projection public int projectionType = Format.NO_VALUE;\n    public float projectionPoseYaw = 0f;\n    public float projectionPosePitch = 0f;\n    public float projectionPoseRoll = 0f;\n    public byte[] projectionData = null;\n    @C.StereoMode\n    public int stereoMode = Format.NO_VALUE;\n    public boolean hasColorInfo = false;\n    @C.ColorSpace\n    public int colorSpace = Format.NO_VALUE;\n    @C.ColorTransfer\n    public int colorTransfer = Format.NO_VALUE;\n    @C.ColorRange\n    public int colorRange = Format.NO_VALUE;\n    public int maxContentLuminance = DEFAULT_MAX_CLL;\n    public int maxFrameAverageLuminance = DEFAULT_MAX_FALL;\n    public float primaryRChromaticityX = Format.NO_VALUE;\n    public float primaryRChromaticityY = Format.NO_VALUE;\n    public float primaryGChromaticityX = Format.NO_VALUE;\n    public float primaryGChromaticityY = Format.NO_VALUE;\n    public float primaryBChromaticityX = Format.NO_VALUE;\n    public float primaryBChromaticityY = Format.NO_VALUE;\n    public float whitePointChromaticityX = Format.NO_VALUE;\n    public float whitePointChromaticityY = Format.NO_VALUE;\n    public float maxMasteringLuminance = Format.NO_VALUE;\n    public float minMasteringLuminance = Format.NO_VALUE;\n\n    // Audio elements. Initially set to their default values.\n    public int channelCount = 1;\n    public int audioBitDepth = Format.NO_VALUE;\n    public int sampleRate = 8000;\n    public long codecDelayNs = 0;\n    public long seekPreRollNs = 0;\n    @Nullable public TrueHdSampleRechunker trueHdSampleRechunker;\n\n    // Text elements.\n    public boolean flagForced;\n    public boolean flagDefault = true;\n    private String language = \"eng\";\n\n    // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.\n    public TrackOutput output;\n    public int nalUnitLengthFieldLength;\n\n    /** Initializes the track with an output. */\n    public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {\n      String mimeType;\n      int maxInputSize = Format.NO_VALUE;\n      @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;\n      List<byte[]> initializationData = null;\n      switch (codecId) {\n        case CODEC_ID_VP8:\n          mimeType = MimeTypes.VIDEO_VP8;\n          break;\n        case CODEC_ID_VP9:\n          mimeType = MimeTypes.VIDEO_VP9;\n          break;\n        case CODEC_ID_AV1:\n          mimeType = MimeTypes.VIDEO_AV1;\n          break;\n        case CODEC_ID_MPEG2:\n          mimeType = MimeTypes.VIDEO_MPEG2;\n          break;\n        case CODEC_ID_MPEG4_SP:\n        case CODEC_ID_MPEG4_ASP:\n        case CODEC_ID_MPEG4_AP:\n          mimeType = MimeTypes.VIDEO_MP4V;\n          initializationData =\n              codecPrivate == null ? null : Collections.singletonList(codecPrivate);\n          break;\n        case CODEC_ID_H264:\n          mimeType = MimeTypes.VIDEO_H264;\n          AvcConfig avcConfig = AvcConfig.parse(new ParsableByteArray(codecPrivate));\n          initializationData = avcConfig.initializationData;\n          nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;\n          break;\n        case CODEC_ID_H265:\n          mimeType = MimeTypes.VIDEO_H265;\n          HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(codecPrivate));\n          initializationData = hevcConfig.initializationData;\n          nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;\n          break;\n        case CODEC_ID_FOURCC:\n          Pair<String, List<byte[]>> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate));\n          mimeType = pair.first;\n          initializationData = pair.second;\n          break;\n        case CODEC_ID_THEORA:\n          // TODO: This can be set to the real mimeType if/when we work out what initializationData\n          // should be set to for this case.\n          mimeType = MimeTypes.VIDEO_UNKNOWN;\n          break;\n        case CODEC_ID_VORBIS:\n          mimeType = MimeTypes.AUDIO_VORBIS;\n          maxInputSize = VORBIS_MAX_INPUT_SIZE;\n          initializationData = parseVorbisCodecPrivate(codecPrivate);\n          break;\n        case CODEC_ID_OPUS:\n          mimeType = MimeTypes.AUDIO_OPUS;\n          maxInputSize = OPUS_MAX_INPUT_SIZE;\n          initializationData = new ArrayList<>(3);\n          initializationData.add(codecPrivate);\n          initializationData.add(\n              ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array());\n          initializationData.add(\n              ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array());\n          break;\n        case CODEC_ID_AAC:\n          mimeType = MimeTypes.AUDIO_AAC;\n          initializationData = Collections.singletonList(codecPrivate);\n          break;\n        case CODEC_ID_MP2:\n          mimeType = MimeTypes.AUDIO_MPEG_L2;\n          maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;\n          break;\n        case CODEC_ID_MP3:\n          mimeType = MimeTypes.AUDIO_MPEG;\n          maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;\n          break;\n        case CODEC_ID_AC3:\n          mimeType = MimeTypes.AUDIO_AC3;\n          break;\n        case CODEC_ID_E_AC3:\n          mimeType = MimeTypes.AUDIO_E_AC3;\n          break;\n        case CODEC_ID_TRUEHD:\n          mimeType = MimeTypes.AUDIO_TRUEHD;\n          trueHdSampleRechunker = new TrueHdSampleRechunker();\n          break;\n        case CODEC_ID_DTS:\n        case CODEC_ID_DTS_EXPRESS:\n          mimeType = MimeTypes.AUDIO_DTS;\n          break;\n        case CODEC_ID_DTS_LOSSLESS:\n          mimeType = MimeTypes.AUDIO_DTS_HD;\n          break;\n        case CODEC_ID_FLAC:\n          mimeType = MimeTypes.AUDIO_FLAC;\n          initializationData = Collections.singletonList(codecPrivate);\n          break;\n        case CODEC_ID_ACM:\n          mimeType = MimeTypes.AUDIO_RAW;\n          if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {\n            pcmEncoding = Util.getPcmEncoding(audioBitDepth);\n            if (pcmEncoding == C.ENCODING_INVALID) {\n              pcmEncoding = Format.NO_VALUE;\n              mimeType = MimeTypes.AUDIO_UNKNOWN;\n              Log.w(TAG, \"Unsupported PCM bit depth: \" + audioBitDepth + \". Setting mimeType to \"\n                  + mimeType);\n            }\n          } else {\n            mimeType = MimeTypes.AUDIO_UNKNOWN;\n            Log.w(TAG, \"Non-PCM MS/ACM is unsupported. Setting mimeType to \" + mimeType);\n          }\n          break;\n        case CODEC_ID_PCM_INT_LIT:\n          mimeType = MimeTypes.AUDIO_RAW;\n          pcmEncoding = Util.getPcmEncoding(audioBitDepth);\n          if (pcmEncoding == C.ENCODING_INVALID) {\n            pcmEncoding = Format.NO_VALUE;\n            mimeType = MimeTypes.AUDIO_UNKNOWN;\n            Log.w(TAG, \"Unsupported PCM bit depth: \" + audioBitDepth + \". Setting mimeType to \"\n                + mimeType);\n          }\n          break;\n        case CODEC_ID_SUBRIP:\n          mimeType = MimeTypes.APPLICATION_SUBRIP;\n          break;\n        case CODEC_ID_ASS:\n          mimeType = MimeTypes.TEXT_SSA;\n          break;\n        case CODEC_ID_VOBSUB:\n          mimeType = MimeTypes.APPLICATION_VOBSUB;\n          initializationData = Collections.singletonList(codecPrivate);\n          break;\n        case CODEC_ID_PGS:\n          mimeType = MimeTypes.APPLICATION_PGS;\n          break;\n        case CODEC_ID_DVBSUB:\n          mimeType = MimeTypes.APPLICATION_DVBSUBS;\n          // Init data: composition_page (2), ancillary_page (2)\n          initializationData = Collections.singletonList(new byte[] {codecPrivate[0],\n              codecPrivate[1], codecPrivate[2], codecPrivate[3]});\n          break;\n        default:\n          throw new ParserException(\"Unrecognized codec identifier.\");\n      }\n\n      int type;\n      Format format;\n      @C.SelectionFlags int selectionFlags = 0;\n      selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;\n      selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;\n      // TODO: Consider reading the name elements of the tracks and, if present, incorporating them\n      // into the trackId passed when creating the formats.\n      if (MimeTypes.isAudio(mimeType)) {\n        type = C.TRACK_TYPE_AUDIO;\n        format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,\n            Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,\n            initializationData, drmInitData, selectionFlags, language);\n      } else if (MimeTypes.isVideo(mimeType)) {\n        type = C.TRACK_TYPE_VIDEO;\n        if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {\n          displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;\n          displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;\n        }\n        float pixelWidthHeightRatio = Format.NO_VALUE;\n        if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) {\n          pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight);\n        }\n        ColorInfo colorInfo = null;\n        if (hasColorInfo) {\n          byte[] hdrStaticInfo = getHdrStaticInfo();\n          colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);\n        }\n        int rotationDegrees = Format.NO_VALUE;\n        // Some HTC devices signal rotation in track names.\n        if (\"htc_video_rotA-000\".equals(name)) {\n          rotationDegrees = 0;\n        } else if (\"htc_video_rotA-090\".equals(name)) {\n          rotationDegrees = 90;\n        } else if (\"htc_video_rotA-180\".equals(name)) {\n          rotationDegrees = 180;\n        } else if (\"htc_video_rotA-270\".equals(name)) {\n          rotationDegrees = 270;\n        }\n        if (projectionType == C.PROJECTION_RECTANGULAR\n            && Float.compare(projectionPoseYaw, 0f) == 0\n            && Float.compare(projectionPosePitch, 0f) == 0) {\n          // The range of projectionPoseRoll is [-180, 180].\n          if (Float.compare(projectionPoseRoll, 0f) == 0) {\n            rotationDegrees = 0;\n          } else if (Float.compare(projectionPosePitch, 90f) == 0) {\n            rotationDegrees = 90;\n          } else if (Float.compare(projectionPosePitch, -180f) == 0\n              || Float.compare(projectionPosePitch, 180f) == 0) {\n            rotationDegrees = 180;\n          } else if (Float.compare(projectionPosePitch, -90f) == 0) {\n            rotationDegrees = 270;\n          }\n        }\n        format =\n            Format.createVideoSampleFormat(\n                Integer.toString(trackId),\n                mimeType,\n                /* codecs= */ null,\n                /* bitrate= */ Format.NO_VALUE,\n                maxInputSize,\n                width,\n                height,\n                /* frameRate= */ Format.NO_VALUE,\n                initializationData,\n                rotationDegrees,\n                pixelWidthHeightRatio,\n                projectionData,\n                stereoMode,\n                colorInfo,\n                drmInitData);\n      } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {\n        type = C.TRACK_TYPE_TEXT;\n        format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags,\n            language, drmInitData);\n      } else if (MimeTypes.TEXT_SSA.equals(mimeType)) {\n        type = C.TRACK_TYPE_TEXT;\n        initializationData = new ArrayList<>(2);\n        initializationData.add(SSA_DIALOGUE_FORMAT);\n        initializationData.add(codecPrivate);\n        format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,\n            Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData,\n            Format.OFFSET_SAMPLE_RELATIVE, initializationData);\n      } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)\n          || MimeTypes.APPLICATION_PGS.equals(mimeType)\n          || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {\n        type = C.TRACK_TYPE_TEXT;\n        format =\n            Format.createImageSampleFormat(\n                Integer.toString(trackId),\n                mimeType,\n                null,\n                Format.NO_VALUE,\n                selectionFlags,\n                initializationData,\n                language,\n                drmInitData);\n      } else {\n        throw new ParserException(\"Unexpected MIME type.\");\n      }\n\n      this.output = output.track(number, type);\n      this.output.format(format);\n    }\n\n    /** Forces any pending sample metadata to be flushed to the output. */\n    public void outputPendingSampleMetadata() {\n      if (trueHdSampleRechunker != null) {\n        trueHdSampleRechunker.outputPendingSampleMetadata(this);\n      }\n    }\n\n    /** Resets any state stored in the track in response to a seek. */\n    public void reset() {\n      if (trueHdSampleRechunker != null) {\n        trueHdSampleRechunker.reset();\n      }\n    }\n\n    /** Returns the HDR Static Info as defined in CTA-861.3. */\n    @Nullable\n    private byte[] getHdrStaticInfo() {\n      // Are all fields present.\n      if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE\n          || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE\n          || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE\n          || whitePointChromaticityX == Format.NO_VALUE\n          || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE\n          || minMasteringLuminance == Format.NO_VALUE) {\n        return null;\n      }\n\n      byte[] hdrStaticInfoData = new byte[25];\n      ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN);\n      hdrStaticInfo.put((byte) 0);  // Type.\n      hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY)  + 0.5f));\n      hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));\n      hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));\n      hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));\n      hdrStaticInfo.putShort((short) maxContentLuminance);\n      hdrStaticInfo.putShort((short) maxFrameAverageLuminance);\n      return hdrStaticInfoData;\n    }\n\n    /**\n     * Builds initialization data for a {@link Format} from FourCC codec private data.\n     *\n     * @return The codec mime type and initialization data. If the compression type is not supported\n     *     then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data\n     *     is {@code null}.\n     * @throws ParserException If the initialization data could not be built.\n     */\n    private static Pair<String, List<byte[]>> parseFourCcPrivate(ParsableByteArray buffer)\n        throws ParserException {\n      try {\n        buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2).\n        long compression = buffer.readLittleEndianUnsignedInt();\n        if (compression == FOURCC_COMPRESSION_DIVX) {\n          return new Pair<>(MimeTypes.VIDEO_DIVX, null);\n        } else if (compression == FOURCC_COMPRESSION_H263) {\n          return new Pair<>(MimeTypes.VIDEO_H263, null);\n        } else if (compression == FOURCC_COMPRESSION_VC1) {\n          // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20\n          // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).\n          int startOffset = buffer.getPosition() + 20;\n          byte[] bufferData = buffer.data;\n          for (int offset = startOffset; offset < bufferData.length - 4; offset++) {\n            if (bufferData[offset] == 0x00\n                && bufferData[offset + 1] == 0x00\n                && bufferData[offset + 2] == 0x01\n                && bufferData[offset + 3] == 0x0F) {\n              // We've found the initialization data.\n              byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);\n              return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData));\n            }\n          }\n          throw new ParserException(\"Failed to find FourCC VC1 initialization data\");\n        }\n      } catch (ArrayIndexOutOfBoundsException e) {\n        throw new ParserException(\"Error parsing FourCC private data\");\n      }\n\n      Log.w(TAG, \"Unknown FourCC. Setting mimeType to \" + MimeTypes.VIDEO_UNKNOWN);\n      return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null);\n    }\n\n    /**\n     * Builds initialization data for a {@link Format} from Vorbis codec private data.\n     *\n     * @return The initialization data for the {@link Format}.\n     * @throws ParserException If the initialization data could not be built.\n     */\n    private static List<byte[]> parseVorbisCodecPrivate(byte[] codecPrivate)\n        throws ParserException {\n      try {\n        if (codecPrivate[0] != 0x02) {\n          throw new ParserException(\"Error parsing vorbis codec private\");\n        }\n        int offset = 1;\n        int vorbisInfoLength = 0;\n        while (codecPrivate[offset] == (byte) 0xFF) {\n          vorbisInfoLength += 0xFF;\n          offset++;\n        }\n        vorbisInfoLength += codecPrivate[offset++];\n\n        int vorbisSkipLength = 0;\n        while (codecPrivate[offset] == (byte) 0xFF) {\n          vorbisSkipLength += 0xFF;\n          offset++;\n        }\n        vorbisSkipLength += codecPrivate[offset++];\n\n        if (codecPrivate[offset] != 0x01) {\n          throw new ParserException(\"Error parsing vorbis codec private\");\n        }\n        byte[] vorbisInfo = new byte[vorbisInfoLength];\n        System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);\n        offset += vorbisInfoLength;\n        if (codecPrivate[offset] != 0x03) {\n          throw new ParserException(\"Error parsing vorbis codec private\");\n        }\n        offset += vorbisSkipLength;\n        if (codecPrivate[offset] != 0x05) {\n          throw new ParserException(\"Error parsing vorbis codec private\");\n        }\n        byte[] vorbisBooks = new byte[codecPrivate.length - offset];\n        System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);\n        List<byte[]> initializationData = new ArrayList<>(2);\n        initializationData.add(vorbisInfo);\n        initializationData.add(vorbisBooks);\n        return initializationData;\n      } catch (ArrayIndexOutOfBoundsException e) {\n        throw new ParserException(\"Error parsing vorbis codec private\");\n      }\n    }\n\n    /**\n     * Parses an MS/ACM codec private, returning whether it indicates PCM audio.\n     *\n     * @return Whether the codec private indicates PCM audio.\n     * @throws ParserException If a parsing error occurs.\n     */\n    private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException {\n      try {\n        int formatTag = buffer.readLittleEndianUnsignedShort();\n        if (formatTag == WAVE_FORMAT_PCM) {\n          return true;\n        } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) {\n          buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4)\n          return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits()\n              && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits();\n        } else {\n          return false;\n        }\n      } catch (ArrayIndexOutOfBoundsException e) {\n        throw new ParserException(\"Error parsing MS/ACM codec private\");\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * Utility class that peeks from the input stream in order to determine whether it appears to be\n * compatible input for this extractor.\n */\n/* package */ final class Sniffer {\n\n  /**\n   * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}.\n   */\n  private static final int SEARCH_LENGTH = 1024;\n  private static final int ID_EBML = 0x1A45DFA3;\n\n  private final ParsableByteArray scratch;\n  private int peekLength;\n\n  public Sniffer() {\n    scratch = new ParsableByteArray(8);\n  }\n\n  /**\n   * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput)\n   */\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH\n        ? SEARCH_LENGTH : inputLength);\n    // Find four bytes equal to ID_EBML near the start of the input.\n    input.peekFully(scratch.data, 0, 4);\n    long tag = scratch.readUnsignedInt();\n    peekLength = 4;\n    while (tag != ID_EBML) {\n      if (++peekLength == bytesToSearch) {\n        return false;\n      }\n      input.peekFully(scratch.data, 0, 1);\n      tag = (tag << 8) & 0xFFFFFF00;\n      tag |= scratch.data[0] & 0xFF;\n    }\n\n    // Read the size of the EBML header and make sure it is within the stream.\n    long headerSize = readUint(input);\n    long headerStart = peekLength;\n    if (headerSize == Long.MIN_VALUE\n        || (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) {\n      return false;\n    }\n\n    // Read the payload elements in the EBML header.\n    while (peekLength < headerStart + headerSize) {\n      long id = readUint(input);\n      if (id == Long.MIN_VALUE) {\n        return false;\n      }\n      long size = readUint(input);\n      if (size < 0 || size > Integer.MAX_VALUE) {\n        return false;\n      }\n      if (size != 0) {\n        int sizeInt = (int) size;\n        input.advancePeekPosition(sizeInt);\n        peekLength += sizeInt;\n      }\n    }\n    return peekLength == headerStart + headerSize;\n  }\n\n  /**\n   * Peeks a variable-length unsigned EBML integer from the input.\n   */\n  private long readUint(ExtractorInput input) throws IOException, InterruptedException {\n    input.peekFully(scratch.data, 0, 1);\n    int value = scratch.data[0] & 0xFF;\n    if (value == 0) {\n      return Long.MIN_VALUE;\n    }\n    int mask = 0x80;\n    int length = 0;\n    while ((value & mask) == 0) {\n      mask >>= 1;\n      length++;\n    }\n    value &= ~mask;\n    input.peekFully(scratch.data, 1, length);\n    for (int i = 0; i < length; i++) {\n      value <<= 8;\n      value += scratch.data[i + 1] & 0xFF;\n    }\n    peekLength += length + 1;\n    return value;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mkv;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/**\n * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}.\n */\n/* package */ final class VarintReader {\n\n  private static final int STATE_BEGIN_READING = 0;\n  private static final int STATE_READ_CONTENTS = 1;\n\n  /**\n   * The first byte of a variable-length integer (varint) will have one of these bit masks\n   * indicating the total length in bytes.\n   *\n   * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.\n   */\n  private static final long[] VARINT_LENGTH_MASKS = new long[] {\n    0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L\n  };\n\n  private final byte[] scratch;\n\n  private int state;\n  private int length;\n\n  public VarintReader() {\n    scratch = new byte[8];\n  }\n\n  /**\n   * Resets the reader to start reading a new variable-length integer.\n   */\n  public void reset() {\n    state = STATE_BEGIN_READING;\n    length = 0;\n  }\n\n  /**\n   * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that\n   * reading can be resumed later if an error occurs having read only some of it.\n   * <p>\n   * If an value is successfully read, then the reader will automatically reset itself ready to\n   * read another value.\n   * <p>\n   * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed\n   * later by calling this method again, passing an {@link ExtractorInput} providing data starting\n   * where the previous one left off.\n   *\n   * @param input The {@link ExtractorInput} from which the integer should be read.\n   * @param allowEndOfInput True if encountering the end of the input having read no data is\n   *     allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it\n   *     should be considered an error, causing an {@link EOFException} to be thrown.\n   * @param removeLengthMask Removes the variable-length integer length mask from the value.\n   * @param maximumAllowedLength Maximum allowed length of the variable integer to be read.\n   * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true\n   *     and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the\n   *     length of the varint exceeded maximumAllowedLength.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput,\n      boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException {\n    if (state == STATE_BEGIN_READING) {\n      // Read the first byte to establish the length.\n      if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      int firstByte = scratch[0] & 0xFF;\n      length = parseUnsignedVarintLength(firstByte);\n      if (length == C.LENGTH_UNSET) {\n        throw new IllegalStateException(\"No valid varint length mask found\");\n      }\n      state = STATE_READ_CONTENTS;\n    }\n\n    if (length > maximumAllowedLength) {\n      state = STATE_BEGIN_READING;\n      return C.RESULT_MAX_LENGTH_EXCEEDED;\n    }\n\n    if (length != 1) {\n      // Read the remaining bytes.\n      input.readFully(scratch, 1, length - 1);\n    }\n\n    state = STATE_BEGIN_READING;\n    return assembleVarint(scratch, length, removeLengthMask);\n  }\n\n  /**\n   * Returns the number of bytes occupied by the most recently parsed varint.\n   */\n  public int getLastLength() {\n    return length;\n  }\n\n  /**\n   * Parses and the length of the varint given the first byte.\n   *\n   * @param firstByte First byte of the varint.\n   * @return Length of the varint beginning with the given byte if it was valid,\n   *     {@link C#LENGTH_UNSET} otherwise.\n   */\n  public static int parseUnsignedVarintLength(int firstByte) {\n    int varIntLength = C.LENGTH_UNSET;\n    for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {\n      if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {\n        varIntLength = i + 1;\n        break;\n      }\n    }\n    return varIntLength;\n  }\n\n  /**\n   * Assemble a varint from the given byte array.\n   *\n   * @param varintBytes Bytes that make up the varint.\n   * @param varintLength Length of the varint to assemble.\n   * @param removeLengthMask Removes the variable-length integer length mask from the value.\n   * @return Parsed and assembled varint.\n   */\n  public static long assembleVarint(byte[] varintBytes, int varintLength,\n      boolean removeLengthMask) {\n    long varint = varintBytes[0] & 0xFFL;\n    if (removeLengthMask) {\n      varint &= ~VARINT_LENGTH_MASKS[varintLength - 1];\n    }\n    for (int i = 1; i < varintLength; i++) {\n      varint = (varint << 8) | (varintBytes[i] & 0xFFL);\n    }\n    return varint;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\n\n/**\n * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.\n */\n/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker {\n\n  /**\n   * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.\n   * @param firstFramePosition The position of the first frame in the stream.\n   * @param mpegAudioHeader The MPEG audio header associated with the first frame.\n   */\n  public ConstantBitrateSeeker(\n      long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) {\n    super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize);\n  }\n\n  @Override\n  public long getTimeUs(long position) {\n    return getTimeUsAtPosition(position);\n  }\n\n  @Override\n  public long getDataEndPosition() {\n    return C.POSITION_UNSET;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.metadata.id3.MlltFrame;\nimport com.google.android.exoplayer2.util.Util;\n\n/** MP3 seeker that uses metadata from an {@link MlltFrame}. */\n/* package */ final class MlltSeeker implements Seeker {\n\n  /**\n   * Returns an {@link MlltSeeker} for seeking in the stream.\n   *\n   * @param firstFramePosition The position of the start of the first frame in the stream.\n   * @param mlltFrame The MLLT frame with seeking metadata.\n   * @return An {@link MlltSeeker} for seeking in the stream.\n   */\n  public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) {\n    int referenceCount = mlltFrame.bytesDeviations.length;\n    long[] referencePositions = new long[1 + referenceCount];\n    long[] referenceTimesMs = new long[1 + referenceCount];\n    referencePositions[0] = firstFramePosition;\n    referenceTimesMs[0] = 0;\n    long position = firstFramePosition;\n    long timeMs = 0;\n    for (int i = 1; i <= referenceCount; i++) {\n      position += mlltFrame.bytesBetweenReference + mlltFrame.bytesDeviations[i - 1];\n      timeMs += mlltFrame.millisecondsBetweenReference + mlltFrame.millisecondsDeviations[i - 1];\n      referencePositions[i] = position;\n      referenceTimesMs[i] = timeMs;\n    }\n    return new MlltSeeker(referencePositions, referenceTimesMs);\n  }\n\n  private final long[] referencePositions;\n  private final long[] referenceTimesMs;\n  private final long durationUs;\n\n  private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) {\n    this.referencePositions = referencePositions;\n    this.referenceTimesMs = referenceTimesMs;\n    // Use the last reference point as the duration, as extrapolating variable bitrate at the end of\n    // the stream may give a large error.\n    durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);\n  }\n\n  @Override\n  public boolean isSeekable() {\n    return true;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    timeUs = Util.constrainValue(timeUs, 0, durationUs);\n    Pair<Long, Long> timeMsAndPosition =\n        linearlyInterpolate(C.usToMs(timeUs), referenceTimesMs, referencePositions);\n    timeUs = C.msToUs(timeMsAndPosition.first);\n    long position = timeMsAndPosition.second;\n    return new SeekPoints(new SeekPoint(timeUs, position));\n  }\n\n  @Override\n  public long getTimeUs(long position) {\n    Pair<Long, Long> positionAndTimeMs =\n        linearlyInterpolate(position, referencePositions, referenceTimesMs);\n    return C.msToUs(positionAndTimeMs.second);\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  /**\n   * Given a set of reference points as coordinates in {@code xReferences} and {@code yReferences}\n   * and an x-axis value, linearly interpolates between corresponding reference points to give a\n   * y-axis value.\n   *\n   * @param x The x-axis value for which a y-axis value is needed.\n   * @param xReferences x coordinates of reference points.\n   * @param yReferences y coordinates of reference points.\n   * @return The linearly interpolated y-axis value.\n   */\n  private static Pair<Long, Long> linearlyInterpolate(\n      long x, long[] xReferences, long[] yReferences) {\n    int previousReferenceIndex =\n        Util.binarySearchFloor(xReferences, x, /* inclusive= */ true, /* stayInBounds= */ true);\n    long xPreviousReference = xReferences[previousReferenceIndex];\n    long yPreviousReference = yReferences[previousReferenceIndex];\n    int nextReferenceIndex = previousReferenceIndex + 1;\n    if (nextReferenceIndex == xReferences.length) {\n      return Pair.create(xPreviousReference, yPreviousReference);\n    } else {\n      long xNextReference = xReferences[nextReferenceIndex];\n      long yNextReference = yReferences[nextReferenceIndex];\n      double proportion =\n          xNextReference == xPreviousReference\n              ? 0.0\n              : ((double) x - xPreviousReference) / (xNextReference - xPreviousReference);\n      long y = (long) (proportion * (yNextReference - yPreviousReference)) + yPreviousReference;\n      return Pair.create(x, y);\n    }\n  }\n\n  @Override\n  public long getDataEndPosition() {\n    return C.POSITION_UNSET;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.GaplessInfoHolder;\nimport com.google.android.exoplayer2.extractor.Id3Peeker;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;\nimport com.google.android.exoplayer2.metadata.id3.MlltFrame;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Extracts data from the MP3 container format.\n */\npublic final class Mp3Extractor implements Extractor {\n\n  /** Factory for {@link Mp3Extractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag values are {@link\n   * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})\n  public @interface Flags {}\n  /**\n   * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would\n   * otherwise not be possible.\n   */\n  public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;\n  /**\n   * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not\n   * required.\n   */\n  public static final int FLAG_DISABLE_ID3_METADATA = 2;\n\n  /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */\n  private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE =\n      (majorVersion, id0, id1, id2, id3) ->\n          ((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2))\n              || (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2)));\n\n  /**\n   * The maximum number of bytes to search when synchronizing, before giving up.\n   */\n  private static final int MAX_SYNC_BYTES = 128 * 1024;\n  /**\n   * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.\n   */\n  private static final int MAX_SNIFF_BYTES = 16 * 1024;\n  /**\n   * Maximum length of data read into {@link #scratch}.\n   */\n  private static final int SCRATCH_LENGTH = 10;\n\n  /**\n   * Mask that includes the audio header values that must match between frames.\n   */\n  private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00;\n\n  private static final int SEEK_HEADER_XING = 0x58696e67;\n  private static final int SEEK_HEADER_INFO = 0x496e666f;\n  private static final int SEEK_HEADER_VBRI = 0x56425249;\n  private static final int SEEK_HEADER_UNSET = 0;\n\n  @Flags private final int flags;\n  private final long forcedFirstSampleTimestampUs;\n  private final ParsableByteArray scratch;\n  private final MpegAudioHeader synchronizedHeader;\n  private final GaplessInfoHolder gaplessInfoHolder;\n  private final Id3Peeker id3Peeker;\n\n  // Extractor outputs.\n  private ExtractorOutput extractorOutput;\n  private TrackOutput trackOutput;\n\n  private int synchronizedHeaderData;\n\n  private Metadata metadata;\n  @Nullable private Seeker seeker;\n  private boolean disableSeeking;\n  private long basisTimeUs;\n  private long samplesRead;\n  private long firstSamplePosition;\n  private int sampleBytesRemaining;\n\n  public Mp3Extractor() {\n    this(0);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public Mp3Extractor(@Flags int flags) {\n    this(flags, C.TIME_UNSET);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or\n   *     {@link C#TIME_UNSET} if forcing is not required.\n   */\n  public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {\n    this.flags = flags;\n    this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;\n    scratch = new ParsableByteArray(SCRATCH_LENGTH);\n    synchronizedHeader = new MpegAudioHeader();\n    gaplessInfoHolder = new GaplessInfoHolder();\n    basisTimeUs = C.TIME_UNSET;\n    id3Peeker = new Id3Peeker();\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return synchronize(input, true);\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    extractorOutput = output;\n    trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);\n    extractorOutput.endTracks();\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    synchronizedHeaderData = 0;\n    basisTimeUs = C.TIME_UNSET;\n    samplesRead = 0;\n    sampleBytesRemaining = 0;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    if (synchronizedHeaderData == 0) {\n      try {\n        synchronize(input, false);\n      } catch (EOFException e) {\n        return RESULT_END_OF_INPUT;\n      }\n    }\n    if (seeker == null) {\n      // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata\n      // takes priority as it can provide greater precision.\n      Seeker seekFrameSeeker = maybeReadSeekFrame(input);\n      Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition());\n\n      if (disableSeeking) {\n        seeker = new UnseekableSeeker();\n      } else {\n        if (metadataSeeker != null) {\n          seeker = metadataSeeker;\n        } else if (seekFrameSeeker != null) {\n          seeker = seekFrameSeeker;\n        }\n        if (seeker == null\n            || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {\n          seeker = getConstantBitrateSeeker(input);\n        }\n      }\n      extractorOutput.seekMap(seeker);\n      trackOutput.format(\n          Format.createAudioSampleFormat(\n              /* id= */ null,\n              synchronizedHeader.mimeType,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              MpegAudioHeader.MAX_FRAME_SIZE_BYTES,\n              synchronizedHeader.channels,\n              synchronizedHeader.sampleRate,\n              /* pcmEncoding= */ Format.NO_VALUE,\n              gaplessInfoHolder.encoderDelay,\n              gaplessInfoHolder.encoderPadding,\n              /* initializationData= */ null,\n              /* drmInitData= */ null,\n              /* selectionFlags= */ 0,\n              /* language= */ null,\n              (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));\n      firstSamplePosition = input.getPosition();\n    } else if (firstSamplePosition != 0) {\n      long inputPosition = input.getPosition();\n      if (inputPosition < firstSamplePosition) {\n        // Skip past the seek frame.\n        input.skipFully((int) (firstSamplePosition - inputPosition));\n      }\n    }\n    return readSample(input);\n  }\n\n  /**\n   * Disables the extractor from being able to seek through the media.\n   *\n   * <p>Please note that this needs to be called before {@link #read}.\n   */\n  public void disableSeeking() {\n    disableSeeking = true;\n  }\n\n  // Internal methods.\n\n  private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {\n    if (sampleBytesRemaining == 0) {\n      extractorInput.resetPeekPosition();\n      if (peekEndOfStreamOrHeader(extractorInput)) {\n        return RESULT_END_OF_INPUT;\n      }\n      scratch.setPosition(0);\n      int sampleHeaderData = scratch.readInt();\n      if (!headersMatch(sampleHeaderData, synchronizedHeaderData)\n          || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {\n        // We have lost synchronization, so attempt to resynchronize starting at the next byte.\n        extractorInput.skipFully(1);\n        synchronizedHeaderData = 0;\n        return RESULT_CONTINUE;\n      }\n      MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);\n      if (basisTimeUs == C.TIME_UNSET) {\n        basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());\n        if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {\n          long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);\n          basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;\n        }\n      }\n      sampleBytesRemaining = synchronizedHeader.frameSize;\n    }\n    int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);\n    if (bytesAppended == C.RESULT_END_OF_INPUT) {\n      return RESULT_END_OF_INPUT;\n    }\n    sampleBytesRemaining -= bytesAppended;\n    if (sampleBytesRemaining > 0) {\n      return RESULT_CONTINUE;\n    }\n    long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate);\n    trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0,\n        null);\n    samplesRead += synchronizedHeader.samplesPerFrame;\n    sampleBytesRemaining = 0;\n    return RESULT_CONTINUE;\n  }\n\n  private boolean synchronize(ExtractorInput input, boolean sniffing)\n      throws IOException, InterruptedException {\n    int validFrameCount = 0;\n    int candidateSynchronizedHeaderData = 0;\n    int peekedId3Bytes = 0;\n    int searchedBytes = 0;\n    int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;\n    input.resetPeekPosition();\n    if (input.getPosition() == 0) {\n      // We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information\n      // even if ID3 metadata parsing is disabled.\n      boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0;\n      FramePredicate id3FramePredicate =\n          parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE;\n      metadata = id3Peeker.peekId3Data(input, id3FramePredicate);\n      if (metadata != null) {\n        gaplessInfoHolder.setFromMetadata(metadata);\n      }\n      peekedId3Bytes = (int) input.getPeekPosition();\n      if (!sniffing) {\n        input.skipFully(peekedId3Bytes);\n      }\n    }\n    while (true) {\n      if (peekEndOfStreamOrHeader(input)) {\n        if (validFrameCount > 0) {\n          // We reached the end of the stream but found at least one valid frame.\n          break;\n        }\n        throw new EOFException();\n      }\n      scratch.setPosition(0);\n      int headerData = scratch.readInt();\n      int frameSize;\n      if ((candidateSynchronizedHeaderData != 0\n          && !headersMatch(headerData, candidateSynchronizedHeaderData))\n          || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {\n        // The header doesn't match the candidate header or is invalid. Try the next byte offset.\n        if (searchedBytes++ == searchLimitBytes) {\n          if (!sniffing) {\n            throw new ParserException(\"Searched too many bytes.\");\n          }\n          return false;\n        }\n        validFrameCount = 0;\n        candidateSynchronizedHeaderData = 0;\n        if (sniffing) {\n          input.resetPeekPosition();\n          input.advancePeekPosition(peekedId3Bytes + searchedBytes);\n        } else {\n          input.skipFully(1);\n        }\n      } else {\n        // The header matches the candidate header and/or is valid.\n        validFrameCount++;\n        if (validFrameCount == 1) {\n          MpegAudioHeader.populateHeader(headerData, synchronizedHeader);\n          candidateSynchronizedHeaderData = headerData;\n        } else if (validFrameCount == 4) {\n          break;\n        }\n        input.advancePeekPosition(frameSize - 4);\n      }\n    }\n    // Prepare to read the synchronized frame.\n    if (sniffing) {\n      input.skipFully(peekedId3Bytes + searchedBytes);\n    } else {\n      input.resetPeekPosition();\n    }\n    synchronizedHeaderData = candidateSynchronizedHeaderData;\n    return true;\n  }\n\n  /**\n   * Returns whether the extractor input is peeking the end of the stream. If {@code false},\n   * populates the scratch buffer with the next four bytes.\n   */\n  private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput)\n      throws IOException, InterruptedException {\n    if (seeker != null) {\n      long dataEndPosition = seeker.getDataEndPosition();\n      if (dataEndPosition != C.POSITION_UNSET\n          && extractorInput.getPeekPosition() > dataEndPosition - 4) {\n        return true;\n      }\n    }\n    try {\n      return !extractorInput.peekFully(\n          scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true);\n    } catch (EOFException e) {\n      return true;\n    }\n  }\n\n  /**\n   * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,\n   * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.\n   * After this method returns, the input position is the start of the first frame of audio.\n   *\n   * @param input The {@link ExtractorInput} from which to read.\n   * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise.\n   * @throws IOException Thrown if there was an error reading from the stream. Not expected if the\n   *     next two frames were already peeked during synchronization.\n   * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if\n   *     the next two frames were already peeked during synchronization.\n   */\n  private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException {\n    ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);\n    input.peekFully(frame.data, 0, synchronizedHeader.frameSize);\n    int xingBase = (synchronizedHeader.version & 1) != 0\n        ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1\n        : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5\n    int seekHeader = getSeekFrameHeader(frame, xingBase);\n    Seeker seeker;\n    if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {\n      seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);\n      if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {\n        // If there is a Xing header, read gapless playback metadata at a fixed offset.\n        input.resetPeekPosition();\n        input.advancePeekPosition(xingBase + 141);\n        input.peekFully(scratch.data, 0, 3);\n        scratch.setPosition(0);\n        gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());\n      }\n      input.skipFully(synchronizedHeader.frameSize);\n      if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) {\n        // Fall back to constant bitrate seeking for Info headers missing a table of contents.\n        return getConstantBitrateSeeker(input);\n      }\n    } else if (seekHeader == SEEK_HEADER_VBRI) {\n      seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);\n      input.skipFully(synchronizedHeader.frameSize);\n    } else { // seekerHeader == SEEK_HEADER_UNSET\n      // This frame doesn't contain seeking information, so reset the peek position.\n      seeker = null;\n      input.resetPeekPosition();\n    }\n    return seeker;\n  }\n\n  /**\n   * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.\n   */\n  private Seeker getConstantBitrateSeeker(ExtractorInput input)\n      throws IOException, InterruptedException {\n    input.peekFully(scratch.data, 0, 4);\n    scratch.setPosition(0);\n    MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);\n    return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);\n  }\n\n  /**\n   * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}.\n   */\n  private static boolean headersMatch(int headerA, long headerB) {\n    return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK);\n  }\n\n  /**\n   * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if\n   * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise.\n   * If seeking metadata is present, {@code frame}'s position is advanced past the header.\n   */\n  private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) {\n    if (frame.limit() >= xingBase + 4) {\n      frame.setPosition(xingBase);\n      int headerData = frame.readInt();\n      if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) {\n        return headerData;\n      }\n    }\n    if (frame.limit() >= 40) {\n      frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.\n      if (frame.readInt() == SEEK_HEADER_VBRI) {\n        return SEEK_HEADER_VBRI;\n      }\n    }\n    return SEEK_HEADER_UNSET;\n  }\n\n  @Nullable\n  private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) {\n    if (metadata != null) {\n      int length = metadata.length();\n      for (int i = 0; i < length; i++) {\n        Metadata.Entry entry = metadata.get(i);\n        if (entry instanceof MlltFrame) {\n          return MlltSeeker.create(firstFramePosition, (MlltFrame) entry);\n        }\n      }\n    }\n    return null;\n  }\n\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.SeekMap;\n\n/**\n * {@link SeekMap} that provides the end position of audio data and also allows mapping from\n * position (byte offset) back to time, which can be used to work out the new sample basis timestamp\n * after seeking and resynchronization.\n */\n/* package */ interface Seeker extends SeekMap {\n\n  /**\n   * Maps a position (byte offset) to a corresponding sample timestamp.\n   *\n   * @param position A seek position (byte offset) relative to the start of the stream.\n   * @return The corresponding timestamp of the next sample to be read, in microseconds.\n   */\n  long getTimeUs(long position);\n\n  /**\n   * Returns the position (byte offset) in the stream that is immediately after audio data, or\n   * {@link C#POSITION_UNSET} if not known.\n   */\n  long getDataEndPosition();\n\n  /** A {@link Seeker} that does not support seeking through audio data. */\n  /* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker {\n\n    public UnseekableSeeker() {\n      super(/* durationUs= */ C.TIME_UNSET);\n    }\n\n    @Override\n    public long getTimeUs(long position) {\n      return 0;\n    }\n\n    @Override\n    public long getDataEndPosition() {\n      // Position unset as we do not know the data end position. Note that returning 0 doesn't work.\n      return C.POSITION_UNSET;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\n\n/** MP3 seeker that uses metadata from a VBRI header. */\n/* package */ final class VbriSeeker implements Seeker {\n\n  private static final String TAG = \"VbriSeeker\";\n\n  /**\n   * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.\n   * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the\n   * caller should reset it.\n   *\n   * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.\n   * @param position The position of the start of this frame in the stream.\n   * @param mpegAudioHeader The MPEG audio header associated with the frame.\n   * @param frame The data in this audio frame, with its position set to immediately after the\n   *     'VBRI' tag.\n   * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required\n   *     information is not present.\n   */\n  public static @Nullable VbriSeeker create(\n      long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {\n    frame.skipBytes(10);\n    int numFrames = frame.readInt();\n    if (numFrames <= 0) {\n      return null;\n    }\n    int sampleRate = mpegAudioHeader.sampleRate;\n    long durationUs = Util.scaleLargeTimestamp(numFrames,\n        C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate);\n    int entryCount = frame.readUnsignedShort();\n    int scale = frame.readUnsignedShort();\n    int entrySize = frame.readUnsignedShort();\n    frame.skipBytes(2);\n\n    long minPosition = position + mpegAudioHeader.frameSize;\n    // Read table of contents entries.\n    long[] timesUs = new long[entryCount];\n    long[] positions = new long[entryCount];\n    for (int index = 0; index < entryCount; index++) {\n      timesUs[index] = (index * durationUs) / entryCount;\n      // Ensure positions do not fall within the frame containing the VBRI header. This constraint\n      // will normally only apply to the first entry in the table.\n      positions[index] = Math.max(position, minPosition);\n      int segmentSize;\n      switch (entrySize) {\n        case 1:\n          segmentSize = frame.readUnsignedByte();\n          break;\n        case 2:\n          segmentSize = frame.readUnsignedShort();\n          break;\n        case 3:\n          segmentSize = frame.readUnsignedInt24();\n          break;\n        case 4:\n          segmentSize = frame.readUnsignedIntToInt();\n          break;\n        default:\n          return null;\n      }\n      position += segmentSize * scale;\n    }\n    if (inputLength != C.LENGTH_UNSET && inputLength != position) {\n      Log.w(TAG, \"VBRI data size mismatch: \" + inputLength + \", \" + position);\n    }\n    return new VbriSeeker(timesUs, positions, durationUs, /* dataEndPosition= */ position);\n  }\n\n  private final long[] timesUs;\n  private final long[] positions;\n  private final long durationUs;\n  private final long dataEndPosition;\n\n  private VbriSeeker(long[] timesUs, long[] positions, long durationUs, long dataEndPosition) {\n    this.timesUs = timesUs;\n    this.positions = positions;\n    this.durationUs = durationUs;\n    this.dataEndPosition = dataEndPosition;\n  }\n\n  @Override\n  public boolean isSeekable() {\n    return true;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true);\n    SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]);\n    if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) {\n      return new SeekPoints(seekPoint);\n    } else {\n      SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]);\n      return new SeekPoints(seekPoint, nextSeekPoint);\n    }\n  }\n\n  @Override\n  public long getTimeUs(long position) {\n    return timesUs[Util.binarySearchFloor(positions, position, true, true)];\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  @Override\n  public long getDataEndPosition() {\n    return dataEndPosition;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp3;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\n\n/** MP3 seeker that uses metadata from a Xing header. */\n/* package */ final class XingSeeker implements Seeker {\n\n  private static final String TAG = \"XingSeeker\";\n\n  /**\n   * Returns a {@link XingSeeker} for seeking in the stream, if required information is present.\n   * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the\n   * caller should reset it.\n   *\n   * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.\n   * @param position The position of the start of this frame in the stream.\n   * @param mpegAudioHeader The MPEG audio header associated with the frame.\n   * @param frame The data in this audio frame, with its position set to immediately after the\n   *     'Xing' or 'Info' tag.\n   * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required\n   *     information is not present.\n   */\n  public static @Nullable XingSeeker create(\n      long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {\n    int samplesPerFrame = mpegAudioHeader.samplesPerFrame;\n    int sampleRate = mpegAudioHeader.sampleRate;\n\n    int flags = frame.readInt();\n    int frameCount;\n    if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {\n      // If the frame count is missing/invalid, the header can't be used to determine the duration.\n      return null;\n    }\n    long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND,\n        sampleRate);\n    if ((flags & 0x06) != 0x06) {\n      // If the size in bytes or table of contents is missing, the stream is not seekable.\n      return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);\n    }\n\n    long dataSize = frame.readUnsignedIntToInt();\n    long[] tableOfContents = new long[100];\n    for (int i = 0; i < 100; i++) {\n      tableOfContents[i] = frame.readUnsignedByte();\n    }\n\n    // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:\n    // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);\n    // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();\n\n    if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) {\n      Log.w(TAG, \"XING data size mismatch: \" + inputLength + \", \" + (position + dataSize));\n    }\n    return new XingSeeker(\n        position, mpegAudioHeader.frameSize, durationUs, dataSize, tableOfContents);\n  }\n\n  private final long dataStartPosition;\n  private final int xingFrameSize;\n  private final long durationUs;\n  /** Data size, including the XING frame. */\n  private final long dataSize;\n\n  private final long dataEndPosition;\n  /**\n   * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the\n   * table of contents was missing from the header, in which case seeking is not be supported.\n   */\n  @Nullable private final long[] tableOfContents;\n\n  private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {\n    this(\n        dataStartPosition,\n        xingFrameSize,\n        durationUs,\n        /* dataSize= */ C.LENGTH_UNSET,\n        /* tableOfContents= */ null);\n  }\n\n  private XingSeeker(\n      long dataStartPosition,\n      int xingFrameSize,\n      long durationUs,\n      long dataSize,\n      @Nullable long[] tableOfContents) {\n    this.dataStartPosition = dataStartPosition;\n    this.xingFrameSize = xingFrameSize;\n    this.durationUs = durationUs;\n    this.tableOfContents = tableOfContents;\n    this.dataSize = dataSize;\n    dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize;\n  }\n\n  @Override\n  public boolean isSeekable() {\n    return tableOfContents != null;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    if (!isSeekable()) {\n      return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize));\n    }\n    timeUs = Util.constrainValue(timeUs, 0, durationUs);\n    double percent = (timeUs * 100d) / durationUs;\n    double scaledPosition;\n    if (percent <= 0) {\n      scaledPosition = 0;\n    } else if (percent >= 100) {\n      scaledPosition = 256;\n    } else {\n      int prevTableIndex = (int) percent;\n      long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents);\n      double prevScaledPosition = tableOfContents[prevTableIndex];\n      double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];\n      // Linearly interpolate between the two scaled positions.\n      double interpolateFraction = percent - prevTableIndex;\n      scaledPosition = prevScaledPosition\n          + (interpolateFraction * (nextScaledPosition - prevScaledPosition));\n    }\n    long positionOffset = Math.round((scaledPosition / 256) * dataSize);\n    // Ensure returned positions skip the frame containing the XING header.\n    positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);\n    return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset));\n  }\n\n  @Override\n  public long getTimeUs(long position) {\n    long positionOffset = position - dataStartPosition;\n    if (!isSeekable() || positionOffset <= xingFrameSize) {\n      return 0L;\n    }\n    long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents);\n    double scaledPosition = (positionOffset * 256d) / dataSize;\n    int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);\n    long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);\n    long prevScaledPosition = tableOfContents[prevTableIndex];\n    long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);\n    long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];\n    // Linearly interpolate between the two table entries.\n    double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0\n        : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));\n    return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  @Override\n  public long getDataEndPosition() {\n    return dataEndPosition;\n  }\n\n  /**\n   * Returns the time in microseconds for a given table index.\n   *\n   * @param tableIndex A table index in the range [0, 100].\n   * @return The corresponding time in microseconds.\n   */\n  private long getTimeUsForTableIndex(int tableIndex) {\n    return (durationUs * tableIndex) / 100;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n@SuppressWarnings(\"ConstantField\")\n/* package */ abstract class Atom {\n\n  /**\n   * Size of an atom header, in bytes.\n   */\n  public static final int HEADER_SIZE = 8;\n\n  /**\n   * Size of a full atom header, in bytes.\n   */\n  public static final int FULL_HEADER_SIZE = 12;\n\n  /**\n   * Size of a long atom header, in bytes.\n   */\n  public static final int LONG_HEADER_SIZE = 16;\n\n  /**\n   * Value for the size field in an atom that defines its size in the largesize field.\n   */\n  public static final int DEFINES_LARGE_SIZE = 1;\n\n  /**\n   * Value for the size field in an atom that extends to the end of the file.\n   */\n  public static final int EXTENDS_TO_END_SIZE = 0;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ftyp = 0x66747970;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avc1 = 0x61766331;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avc3 = 0x61766333;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avcC = 0x61766343;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hvc1 = 0x68766331;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hev1 = 0x68657631;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hvcC = 0x68766343;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vp08 = 0x76703038;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vp09 = 0x76703039;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vpcC = 0x76706343;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_av01 = 0x61763031;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_av1C = 0x61763143;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvav = 0x64766176;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dva1 = 0x64766131;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvhe = 0x64766865;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvh1 = 0x64766831;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvcC = 0x64766343;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvvC = 0x64767643;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_s263 = 0x73323633;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_d263 = 0x64323633;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdat = 0x6d646174;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mp4a = 0x6d703461;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE__mp3 = 0x2e6d7033;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_wave = 0x77617665;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_lpcm = 0x6c70636d;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sowt = 0x736f7774;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ac_3 = 0x61632d33;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dac3 = 0x64616333;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ec_3 = 0x65632d33;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dec3 = 0x64656333;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ac_4 = 0x61632d34;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dac4 = 0x64616334;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsc = 0x64747363;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsh = 0x64747368;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsl = 0x6474736c;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtse = 0x64747365;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ddts = 0x64647473;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tfdt = 0x74666474;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tfhd = 0x74666864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trex = 0x74726578;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trun = 0x7472756e;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sidx = 0x73696478;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_moov = 0x6d6f6f76;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mvhd = 0x6d766864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trak = 0x7472616b;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdia = 0x6d646961;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_minf = 0x6d696e66;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stbl = 0x7374626c;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_esds = 0x65736473;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_moof = 0x6d6f6f66;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_traf = 0x74726166;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mvex = 0x6d766578;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mehd = 0x6d656864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tkhd = 0x746b6864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_edts = 0x65647473;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_elst = 0x656c7374;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdhd = 0x6d646864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hdlr = 0x68646c72;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsd = 0x73747364;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_pssh = 0x70737368;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sinf = 0x73696e66;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_schm = 0x7363686d;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_schi = 0x73636869;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tenc = 0x74656e63;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_encv = 0x656e6376;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_enca = 0x656e6361;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_frma = 0x66726d61;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_saiz = 0x7361697a;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_saio = 0x7361696f;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sbgp = 0x73626770;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sgpd = 0x73677064;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_uuid = 0x75756964;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_senc = 0x73656e63;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_pasp = 0x70617370;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_TTML = 0x54544d4c;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vmhd = 0x766d6864;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mp4v = 0x6d703476;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stts = 0x73747473;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stss = 0x73747373;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ctts = 0x63747473;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsc = 0x73747363;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsz = 0x7374737a;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stz2 = 0x73747a32;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stco = 0x7374636f;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_co64 = 0x636f3634;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tx3g = 0x74783367;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_wvtt = 0x77767474;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stpp = 0x73747070;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_c608 = 0x63363038;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_samr = 0x73616d72;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sawb = 0x73617762;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_udta = 0x75647461;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_meta = 0x6d657461;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_keys = 0x6b657973;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ilst = 0x696c7374;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mean = 0x6d65616e;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_name = 0x6e616d65;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_data = 0x64617461;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_emsg = 0x656d7367;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_st3d = 0x73743364;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sv3d = 0x73763364;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_proj = 0x70726f6a;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_camm = 0x63616d6d;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_alac = 0x616c6163;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_alaw = 0x616c6177;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ulaw = 0x756c6177;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_Opus = 0x4f707573;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dOps = 0x644f7073;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_fLaC = 0x664c6143;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dfLa = 0x64664c61;\n\n  public final int type;\n\n  public Atom(int type) {\n    this.type = type;\n  }\n\n  @Override\n  public String toString() {\n    return getAtomTypeString(type);\n  }\n\n  /**\n   * An MP4 atom that is a leaf.\n   */\n  /* package */ static final class LeafAtom extends Atom {\n\n    /**\n     * The atom data.\n     */\n    public final ParsableByteArray data;\n\n    /**\n     * @param type The type of the atom.\n     * @param data The atom data.\n     */\n    public LeafAtom(int type, ParsableByteArray data) {\n      super(type);\n      this.data = data;\n    }\n\n  }\n\n  /**\n   * An MP4 atom that has child atoms.\n   */\n  /* package */ static final class ContainerAtom extends Atom {\n\n    public final long endPosition;\n    public final List<LeafAtom> leafChildren;\n    public final List<ContainerAtom> containerChildren;\n\n    /**\n     * @param type The type of the atom.\n     * @param endPosition The position of the first byte after the end of the atom.\n     */\n    public ContainerAtom(int type, long endPosition) {\n      super(type);\n      this.endPosition = endPosition;\n      leafChildren = new ArrayList<>();\n      containerChildren = new ArrayList<>();\n    }\n\n    /**\n     * Adds a child leaf to this container.\n     *\n     * @param atom The child to add.\n     */\n    public void add(LeafAtom atom) {\n      leafChildren.add(atom);\n    }\n\n    /**\n     * Adds a child container to this container.\n     *\n     * @param atom The child to add.\n     */\n    public void add(ContainerAtom atom) {\n      containerChildren.add(atom);\n    }\n\n    /**\n     * Returns the child leaf of the given type.\n     *\n     * <p>If no child exists with the given type then null is returned. If multiple children exist\n     * with the given type then the first one to have been added is returned.\n     *\n     * @param type The leaf type.\n     * @return The child leaf of the given type, or null if no such child exists.\n     */\n    @Nullable\n    public LeafAtom getLeafAtomOfType(int type) {\n      int childrenSize = leafChildren.size();\n      for (int i = 0; i < childrenSize; i++) {\n        LeafAtom atom = leafChildren.get(i);\n        if (atom.type == type) {\n          return atom;\n        }\n      }\n      return null;\n    }\n\n    /**\n     * Returns the child container of the given type.\n     *\n     * <p>If no child exists with the given type then null is returned. If multiple children exist\n     * with the given type then the first one to have been added is returned.\n     *\n     * @param type The container type.\n     * @return The child container of the given type, or null if no such child exists.\n     */\n    @Nullable\n    public ContainerAtom getContainerAtomOfType(int type) {\n      int childrenSize = containerChildren.size();\n      for (int i = 0; i < childrenSize; i++) {\n        ContainerAtom atom = containerChildren.get(i);\n        if (atom.type == type) {\n          return atom;\n        }\n      }\n      return null;\n    }\n\n    /**\n     * Returns the total number of leaf/container children of this atom with the given type.\n     *\n     * @param type The type of child atoms to count.\n     * @return The total number of leaf/container children of this atom with the given type.\n     */\n    public int getChildAtomOfTypeCount(int type) {\n      int count = 0;\n      int size = leafChildren.size();\n      for (int i = 0; i < size; i++) {\n        LeafAtom atom = leafChildren.get(i);\n        if (atom.type == type) {\n          count++;\n        }\n      }\n      size = containerChildren.size();\n      for (int i = 0; i < size; i++) {\n        ContainerAtom atom = containerChildren.get(i);\n        if (atom.type == type) {\n          count++;\n        }\n      }\n      return count;\n    }\n\n    @Override\n    public String toString() {\n      return getAtomTypeString(type)\n          + \" leaves: \" + Arrays.toString(leafChildren.toArray())\n          + \" containers: \" + Arrays.toString(containerChildren.toArray());\n    }\n\n  }\n\n  /**\n   * Parses the version number out of the additional integer component of a full atom.\n   */\n  public static int parseFullAtomVersion(int fullAtomInt) {\n    return 0x000000FF & (fullAtomInt >> 24);\n  }\n\n  /**\n   * Parses the atom flags out of the additional integer component of a full atom.\n   */\n  public static int parseFullAtomFlags(int fullAtomInt) {\n    return 0x00FFFFFF & fullAtomInt;\n  }\n\n  /**\n   * Converts a numeric atom type to the corresponding four character string.\n   *\n   * @param type The numeric atom type.\n   * @return The corresponding four character string.\n   */\n  public static String getAtomTypeString(int type) {\n    return \"\" + (char) ((type >> 24) & 0xFF)\n        + (char) ((type >> 16) & 0xFF)\n        + (char) ((type >> 8) & 0xFF)\n        + (char) (type & 0xFF);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.Ac3Util;\nimport com.google.android.exoplayer2.audio.Ac4Util;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.extractor.GaplessInfoHolder;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.AvcConfig;\nimport com.google.android.exoplayer2.video.DolbyVisionConfig;\nimport com.google.android.exoplayer2.video.HevcConfig;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */\n@SuppressWarnings({\"ConstantField\"})\n/* package */ final class AtomParsers {\n\n  private static final String TAG = \"AtomParsers\";\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_vide = 0x76696465;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_soun = 0x736f756e;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_text = 0x74657874;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_sbtl = 0x7362746c;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_subt = 0x73756274;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_clcp = 0x636c6370;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_meta = 0x6d657461;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_mdta = 0x6d647461;\n\n  /**\n   * The threshold number of samples to trim from the start/end of an audio track when applying an\n   * edit below which gapless info can be used (rather than removing samples from the sample table).\n   */\n  private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4;\n\n  /** The magic signature for an Opus Identification header, as defined in RFC-7845. */\n  private static final byte[] opusMagic = Util.getUtf8Bytes(\"OpusHead\");\n\n  /**\n   * Parses a trak atom (defined in 14496-12).\n   *\n   * @param trak Atom to decode.\n   * @param mvhd Movie header atom, used to get the timescale.\n   * @param duration The duration in units of the timescale declared in the mvhd atom, or\n   *     {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @param ignoreEditLists Whether to ignore any edit lists in the trak box.\n   * @param isQuickTime True for QuickTime media. False otherwise.\n   * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.\n   */\n  public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration,\n      DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)\n      throws ParserException {\n    Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);\n    int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));\n    if (trackType == C.TRACK_TYPE_UNKNOWN) {\n      return null;\n    }\n\n    TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);\n    if (duration == C.TIME_UNSET) {\n      duration = tkhdData.duration;\n    }\n    long movieTimescale = parseMvhd(mvhd.data);\n    long durationUs;\n    if (duration == C.TIME_UNSET) {\n      durationUs = C.TIME_UNSET;\n    } else {\n      durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);\n    }\n    Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)\n        .getContainerAtomOfType(Atom.TYPE_stbl);\n\n    Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);\n    StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id,\n        tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime);\n    long[] editListDurations = null;\n    long[] editListMediaTimes = null;\n    if (!ignoreEditLists) {\n      Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));\n      editListDurations = edtsData.first;\n      editListMediaTimes = edtsData.second;\n    }\n    return stsdData.format == null ? null\n        : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,\n            stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,\n            stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes);\n  }\n\n  /**\n   * Parses an stbl atom (defined in 14496-12).\n   *\n   * @param track Track to which this sample table corresponds.\n   * @param stblAtom stbl (sample table) atom to decode.\n   * @param gaplessInfoHolder Holder to populate with gapless playback information.\n   * @return Sample table described by the stbl atom.\n   * @throws ParserException Thrown if the stbl atom can't be parsed.\n   */\n  public static TrackSampleTable parseStbl(\n      Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)\n      throws ParserException {\n    SampleSizeBox sampleSizeBox;\n    Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);\n    if (stszAtom != null) {\n      sampleSizeBox = new StszSampleSizeBox(stszAtom);\n    } else {\n      Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2);\n      if (stz2Atom == null) {\n        throw new ParserException(\"Track has no sample table size information\");\n      }\n      sampleSizeBox = new Stz2SampleSizeBox(stz2Atom);\n    }\n\n    int sampleCount = sampleSizeBox.getSampleCount();\n    if (sampleCount == 0) {\n      return new TrackSampleTable(\n          track,\n          /* offsets= */ new long[0],\n          /* sizes= */ new int[0],\n          /* maximumSize= */ 0,\n          /* timestampsUs= */ new long[0],\n          /* flags= */ new int[0],\n          /* durationUs= */ C.TIME_UNSET);\n    }\n\n    // Entries are byte offsets of chunks.\n    boolean chunkOffsetsAreLongs = false;\n    Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);\n    if (chunkOffsetsAtom == null) {\n      chunkOffsetsAreLongs = true;\n      chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);\n    }\n    ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;\n    // Entries are (chunk number, number of samples per chunk, sample description index).\n    ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;\n    // Entries are (number of samples, timestamp delta between those samples).\n    ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;\n    // Entries are the indices of samples that are synchronization samples.\n    Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);\n    ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;\n    // Entries are (number of samples, timestamp offset).\n    Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);\n    ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;\n\n    // Prepare to read chunk information.\n    ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);\n\n    // Prepare to read sample timestamps.\n    stts.setPosition(Atom.FULL_HEADER_SIZE);\n    int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;\n    int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();\n    int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();\n\n    // Prepare to read sample timestamp offsets, if ctts is present.\n    int remainingSamplesAtTimestampOffset = 0;\n    int remainingTimestampOffsetChanges = 0;\n    int timestampOffset = 0;\n    if (ctts != null) {\n      ctts.setPosition(Atom.FULL_HEADER_SIZE);\n      remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();\n    }\n\n    int nextSynchronizationSampleIndex = C.INDEX_UNSET;\n    int remainingSynchronizationSamples = 0;\n    if (stss != null) {\n      stss.setPosition(Atom.FULL_HEADER_SIZE);\n      remainingSynchronizationSamples = stss.readUnsignedIntToInt();\n      if (remainingSynchronizationSamples > 0) {\n        nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;\n      } else {\n        // Ignore empty stss boxes, which causes all samples to be treated as sync samples.\n        stss = null;\n      }\n    }\n\n    // Fixed sample size raw audio may need to be rechunked.\n    boolean isFixedSampleSizeRawAudio =\n        sampleSizeBox.isFixedSampleSize()\n            && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)\n            && remainingTimestampDeltaChanges == 0\n            && remainingTimestampOffsetChanges == 0\n            && remainingSynchronizationSamples == 0;\n\n    long[] offsets;\n    int[] sizes;\n    int maximumSize = 0;\n    long[] timestamps;\n    int[] flags;\n    long timestampTimeUnits = 0;\n    long duration;\n\n    if (!isFixedSampleSizeRawAudio) {\n      offsets = new long[sampleCount];\n      sizes = new int[sampleCount];\n      timestamps = new long[sampleCount];\n      flags = new int[sampleCount];\n      long offset = 0;\n      int remainingSamplesInChunk = 0;\n\n      for (int i = 0; i < sampleCount; i++) {\n        // Advance to the next chunk if necessary.\n        boolean chunkDataComplete = true;\n        while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) {\n          offset = chunkIterator.offset;\n          remainingSamplesInChunk = chunkIterator.numSamples;\n        }\n        if (!chunkDataComplete) {\n          Log.w(TAG, \"Unexpected end of chunk data\");\n          sampleCount = i;\n          offsets = Arrays.copyOf(offsets, sampleCount);\n          sizes = Arrays.copyOf(sizes, sampleCount);\n          timestamps = Arrays.copyOf(timestamps, sampleCount);\n          flags = Arrays.copyOf(flags, sampleCount);\n          break;\n        }\n\n        // Add on the timestamp offset if ctts is present.\n        if (ctts != null) {\n          while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {\n            remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();\n            // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers\n            // in version 0 ctts boxes, however some streams violate the spec and use signed\n            // integers instead. It's safe to always decode sample offsets as signed integers here,\n            // because unsigned integers will still be parsed correctly (unless their top bit is\n            // set, which is never true in practice because sample offsets are always small).\n            timestampOffset = ctts.readInt();\n            remainingTimestampOffsetChanges--;\n          }\n          remainingSamplesAtTimestampOffset--;\n        }\n\n        offsets[i] = offset;\n        sizes[i] = sampleSizeBox.readNextSampleSize();\n        if (sizes[i] > maximumSize) {\n          maximumSize = sizes[i];\n        }\n        timestamps[i] = timestampTimeUnits + timestampOffset;\n\n        // All samples are synchronization samples if the stss is not present.\n        flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;\n        if (i == nextSynchronizationSampleIndex) {\n          flags[i] = C.BUFFER_FLAG_KEY_FRAME;\n          remainingSynchronizationSamples--;\n          if (remainingSynchronizationSamples > 0) {\n            nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;\n          }\n        }\n\n        // Add on the duration of this sample.\n        timestampTimeUnits += timestampDeltaInTimeUnits;\n        remainingSamplesAtTimestampDelta--;\n        if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {\n          remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();\n          // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers\n          // in stts boxes, however some streams violate the spec and use signed integers instead.\n          // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample\n          // deltas as signed integers here, because unsigned integers will still be parsed\n          // correctly (unless their top bit is set, which is never true in practice because sample\n          // deltas are always small).\n          timestampDeltaInTimeUnits = stts.readInt();\n          remainingTimestampDeltaChanges--;\n        }\n\n        offset += sizes[i];\n        remainingSamplesInChunk--;\n      }\n      duration = timestampTimeUnits + timestampOffset;\n\n      // If the stbl's child boxes are not consistent the container is malformed, but the stream may\n      // still be playable.\n      boolean isCttsValid = true;\n      while (remainingTimestampOffsetChanges > 0) {\n        if (ctts.readUnsignedIntToInt() != 0) {\n          isCttsValid = false;\n          break;\n        }\n        ctts.readInt(); // Ignore offset.\n        remainingTimestampOffsetChanges--;\n      }\n      if (remainingSynchronizationSamples != 0\n          || remainingSamplesAtTimestampDelta != 0\n          || remainingSamplesInChunk != 0\n          || remainingTimestampDeltaChanges != 0\n          || remainingSamplesAtTimestampOffset != 0\n          || !isCttsValid) {\n        Log.w(\n            TAG,\n            \"Inconsistent stbl box for track \"\n                + track.id\n                + \": remainingSynchronizationSamples \"\n                + remainingSynchronizationSamples\n                + \", remainingSamplesAtTimestampDelta \"\n                + remainingSamplesAtTimestampDelta\n                + \", remainingSamplesInChunk \"\n                + remainingSamplesInChunk\n                + \", remainingTimestampDeltaChanges \"\n                + remainingTimestampDeltaChanges\n                + \", remainingSamplesAtTimestampOffset \"\n                + remainingSamplesAtTimestampOffset\n                + (!isCttsValid ? \", ctts invalid\" : \"\"));\n      }\n    } else {\n      long[] chunkOffsetsBytes = new long[chunkIterator.length];\n      int[] chunkSampleCounts = new int[chunkIterator.length];\n      while (chunkIterator.moveNext()) {\n        chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;\n        chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;\n      }\n      int fixedSampleSize =\n          Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);\n      FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(\n          fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);\n      offsets = rechunkedResults.offsets;\n      sizes = rechunkedResults.sizes;\n      maximumSize = rechunkedResults.maximumSize;\n      timestamps = rechunkedResults.timestamps;\n      flags = rechunkedResults.flags;\n      duration = rechunkedResults.duration;\n    }\n    long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);\n\n    if (track.editListDurations == null) {\n      Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);\n      return new TrackSampleTable(\n          track, offsets, sizes, maximumSize, timestamps, flags, durationUs);\n    }\n\n    // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a\n    // sync sample after reordering are not supported. Partial audio sample truncation is only\n    // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES\n    // samples from the start/end of the track. This implementation handles simple\n    // discarding/delaying of samples. The extractor may place further restrictions on what edited\n    // streams are playable.\n\n    if (track.editListDurations.length == 1\n        && track.type == C.TRACK_TYPE_AUDIO\n        && timestamps.length >= 2) {\n      long editStartTime = track.editListMediaTimes[0];\n      long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],\n          track.timescale, track.movieTimescale);\n      if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) {\n        long paddingTimeUnits = duration - editEndTime;\n        long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],\n            track.format.sampleRate, track.timescale);\n        long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,\n            track.format.sampleRate, track.timescale);\n        if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE\n            && encoderPadding <= Integer.MAX_VALUE) {\n          gaplessInfoHolder.encoderDelay = (int) encoderDelay;\n          gaplessInfoHolder.encoderPadding = (int) encoderPadding;\n          Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);\n          long editedDurationUs =\n              Util.scaleLargeTimestamp(\n                  track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale);\n          return new TrackSampleTable(\n              track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs);\n        }\n      }\n    }\n\n    if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {\n      // The current version of the spec leaves handling of an edit with zero segment_duration in\n      // unfragmented files open to interpretation. We handle this as a special case and include all\n      // samples in the edit.\n      long editStartTime = track.editListMediaTimes[0];\n      for (int i = 0; i < timestamps.length; i++) {\n        timestamps[i] =\n            Util.scaleLargeTimestamp(\n                timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);\n      }\n      durationUs =\n          Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);\n      return new TrackSampleTable(\n          track, offsets, sizes, maximumSize, timestamps, flags, durationUs);\n    }\n\n    // Omit any sample at the end point of an edit for audio tracks.\n    boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;\n\n    // Count the number of samples after applying edits.\n    int editedSampleCount = 0;\n    int nextSampleIndex = 0;\n    boolean copyMetadata = false;\n    int[] startIndices = new int[track.editListDurations.length];\n    int[] endIndices = new int[track.editListDurations.length];\n    for (int i = 0; i < track.editListDurations.length; i++) {\n      long editMediaTime = track.editListMediaTimes[i];\n      if (editMediaTime != -1) {\n        long editDuration =\n            Util.scaleLargeTimestamp(\n                track.editListDurations[i], track.timescale, track.movieTimescale);\n        startIndices[i] = Util.binarySearchCeil(timestamps, editMediaTime, true, true);\n        endIndices[i] =\n            Util.binarySearchCeil(\n                timestamps, editMediaTime + editDuration, omitClippedSample, false);\n        while (startIndices[i] < endIndices[i]\n            && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) {\n          // Applying the edit correctly would require prerolling from the previous sync sample. In\n          // the current implementation we advance to the next sync sample instead. Only other\n          // tracks (i.e. audio) will be rendered until the time of the first sync sample.\n          // See https://github.com/google/ExoPlayer/issues/1659.\n          startIndices[i]++;\n        }\n        editedSampleCount += endIndices[i] - startIndices[i];\n        copyMetadata |= nextSampleIndex != startIndices[i];\n        nextSampleIndex = endIndices[i];\n      }\n    }\n    copyMetadata |= editedSampleCount != sampleCount;\n\n    // Calculate edited sample timestamps and update the corresponding metadata arrays.\n    long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;\n    int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;\n    int editedMaximumSize = copyMetadata ? 0 : maximumSize;\n    int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;\n    long[] editedTimestamps = new long[editedSampleCount];\n    long pts = 0;\n    int sampleIndex = 0;\n    for (int i = 0; i < track.editListDurations.length; i++) {\n      long editMediaTime = track.editListMediaTimes[i];\n      int startIndex = startIndices[i];\n      int endIndex = endIndices[i];\n      if (copyMetadata) {\n        int count = endIndex - startIndex;\n        System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);\n        System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);\n        System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);\n      }\n      for (int j = startIndex; j < endIndex; j++) {\n        long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);\n        long timeInSegmentUs =\n            Util.scaleLargeTimestamp(\n                timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale);\n        editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;\n        if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {\n          editedMaximumSize = sizes[j];\n        }\n        sampleIndex++;\n      }\n      pts += track.editListDurations[i];\n    }\n    long editedDurationUs =\n        Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);\n    return new TrackSampleTable(\n        track,\n        editedOffsets,\n        editedSizes,\n        editedMaximumSize,\n        editedTimestamps,\n        editedFlags,\n        editedDurationUs);\n  }\n\n  /**\n   * Parses a udta atom.\n   *\n   * @param udtaAtom The udta (user data) atom to decode.\n   * @param isQuickTime True for QuickTime media. False otherwise.\n   * @return Parsed metadata, or null.\n   */\n  @Nullable\n  public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {\n    if (isQuickTime) {\n      // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and\n      // decode one.\n      return null;\n    }\n    ParsableByteArray udtaData = udtaAtom.data;\n    udtaData.setPosition(Atom.HEADER_SIZE);\n    while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {\n      int atomPosition = udtaData.getPosition();\n      int atomSize = udtaData.readInt();\n      int atomType = udtaData.readInt();\n      if (atomType == Atom.TYPE_meta) {\n        udtaData.setPosition(atomPosition);\n        return parseUdtaMeta(udtaData, atomPosition + atomSize);\n      }\n      udtaData.setPosition(atomPosition + atomSize);\n    }\n    return null;\n  }\n\n  /**\n   * Parses a metadata meta atom if it contains metadata with handler 'mdta'.\n   *\n   * @param meta The metadata atom to decode.\n   * @return Parsed metadata, or null.\n   */\n  @Nullable\n  public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {\n    Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);\n    Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);\n    Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);\n    if (hdlrAtom == null\n        || keysAtom == null\n        || ilstAtom == null\n        || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) {\n      // There isn't enough information to parse the metadata, or the handler type is unexpected.\n      return null;\n    }\n\n    // Parse metadata keys.\n    ParsableByteArray keys = keysAtom.data;\n    keys.setPosition(Atom.FULL_HEADER_SIZE);\n    int entryCount = keys.readInt();\n    String[] keyNames = new String[entryCount];\n    for (int i = 0; i < entryCount; i++) {\n      int entrySize = keys.readInt();\n      keys.skipBytes(4); // keyNamespace\n      int keySize = entrySize - 8;\n      keyNames[i] = keys.readString(keySize);\n    }\n\n    // Parse metadata items.\n    ParsableByteArray ilst = ilstAtom.data;\n    ilst.setPosition(Atom.HEADER_SIZE);\n    ArrayList<Metadata.Entry> entries = new ArrayList<>();\n    while (ilst.bytesLeft() > Atom.HEADER_SIZE) {\n      int atomPosition = ilst.getPosition();\n      int atomSize = ilst.readInt();\n      int keyIndex = ilst.readInt() - 1;\n      if (keyIndex >= 0 && keyIndex < keyNames.length) {\n        String key = keyNames[keyIndex];\n        Metadata.Entry entry =\n            MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key);\n        if (entry != null) {\n          entries.add(entry);\n        }\n      } else {\n        Log.w(TAG, \"Skipped metadata with unknown key index: \" + keyIndex);\n      }\n      ilst.setPosition(atomPosition + atomSize);\n    }\n    return entries.isEmpty() ? null : new Metadata(entries);\n  }\n\n  @Nullable\n  private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) {\n    meta.skipBytes(Atom.FULL_HEADER_SIZE);\n    while (meta.getPosition() < limit) {\n      int atomPosition = meta.getPosition();\n      int atomSize = meta.readInt();\n      int atomType = meta.readInt();\n      if (atomType == Atom.TYPE_ilst) {\n        meta.setPosition(atomPosition);\n        return parseIlst(meta, atomPosition + atomSize);\n      }\n      meta.setPosition(atomPosition + atomSize);\n    }\n    return null;\n  }\n\n  @Nullable\n  private static Metadata parseIlst(ParsableByteArray ilst, int limit) {\n    ilst.skipBytes(Atom.HEADER_SIZE);\n    ArrayList<Metadata.Entry> entries = new ArrayList<>();\n    while (ilst.getPosition() < limit) {\n      Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);\n      if (entry != null) {\n        entries.add(entry);\n      }\n    }\n    return entries.isEmpty() ? null : new Metadata(entries);\n  }\n\n  /**\n   * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.\n   *\n   * @param mvhd Contents of the mvhd atom to be parsed.\n   * @return Timescale for the movie.\n   */\n  private static long parseMvhd(ParsableByteArray mvhd) {\n    mvhd.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = mvhd.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    mvhd.skipBytes(version == 0 ? 8 : 16);\n    return mvhd.readUnsignedInt();\n  }\n\n  /**\n   * Parses a tkhd atom (defined in 14496-12).\n   *\n   * @return An object containing the parsed data.\n   */\n  private static TkhdData parseTkhd(ParsableByteArray tkhd) {\n    tkhd.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = tkhd.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n\n    tkhd.skipBytes(version == 0 ? 8 : 16);\n    int trackId = tkhd.readInt();\n\n    tkhd.skipBytes(4);\n    boolean durationUnknown = true;\n    int durationPosition = tkhd.getPosition();\n    int durationByteCount = version == 0 ? 4 : 8;\n    for (int i = 0; i < durationByteCount; i++) {\n      if (tkhd.data[durationPosition + i] != -1) {\n        durationUnknown = false;\n        break;\n      }\n    }\n    long duration;\n    if (durationUnknown) {\n      tkhd.skipBytes(durationByteCount);\n      duration = C.TIME_UNSET;\n    } else {\n      duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();\n      if (duration == 0) {\n        // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media\n        // samples are in fragments). Treat as unknown.\n        duration = C.TIME_UNSET;\n      }\n    }\n\n    tkhd.skipBytes(16);\n    int a00 = tkhd.readInt();\n    int a01 = tkhd.readInt();\n    tkhd.skipBytes(4);\n    int a10 = tkhd.readInt();\n    int a11 = tkhd.readInt();\n\n    int rotationDegrees;\n    int fixedOne = 65536;\n    if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {\n      rotationDegrees = 90;\n    } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {\n      rotationDegrees = 270;\n    } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {\n      rotationDegrees = 180;\n    } else {\n      // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.\n      rotationDegrees = 0;\n    }\n\n    return new TkhdData(trackId, duration, rotationDegrees);\n  }\n\n  /**\n   * Parses an hdlr atom.\n   *\n   * @param hdlr The hdlr atom to decode.\n   * @return The handler value.\n   */\n  private static int parseHdlr(ParsableByteArray hdlr) {\n    hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);\n    return hdlr.readInt();\n  }\n\n  /** Returns the track type for a given handler value. */\n  private static int getTrackTypeForHdlr(int hdlr) {\n    if (hdlr == TYPE_soun) {\n      return C.TRACK_TYPE_AUDIO;\n    } else if (hdlr == TYPE_vide) {\n      return C.TRACK_TYPE_VIDEO;\n    } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {\n      return C.TRACK_TYPE_TEXT;\n    } else if (hdlr == TYPE_meta) {\n      return C.TRACK_TYPE_METADATA;\n    } else {\n      return C.TRACK_TYPE_UNKNOWN;\n    }\n  }\n\n  /**\n   * Parses an mdhd atom (defined in 14496-12).\n   *\n   * @param mdhd The mdhd atom to decode.\n   * @return A pair consisting of the media timescale defined as the number of time units that pass\n   * in one second, and the language code.\n   */\n  private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {\n    mdhd.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = mdhd.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    mdhd.skipBytes(version == 0 ? 8 : 16);\n    long timescale = mdhd.readUnsignedInt();\n    mdhd.skipBytes(version == 0 ? 4 : 8);\n    int languageCode = mdhd.readUnsignedShort();\n    String language =\n        \"\"\n            + (char) (((languageCode >> 10) & 0x1F) + 0x60)\n            + (char) (((languageCode >> 5) & 0x1F) + 0x60)\n            + (char) ((languageCode & 0x1F) + 0x60);\n    return Pair.create(timescale, language);\n  }\n\n  /**\n   * Parses a stsd atom (defined in 14496-12).\n   *\n   * @param stsd The stsd atom to decode.\n   * @param trackId The track's identifier in its container.\n   * @param rotationDegrees The rotation of the track in degrees.\n   * @param language The language of the track.\n   * @param drmInitData {@link DrmInitData} to be included in the format.\n   * @param isQuickTime True for QuickTime media. False otherwise.\n   * @return An object containing the parsed data.\n   */\n  private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees,\n      String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException {\n    stsd.setPosition(Atom.FULL_HEADER_SIZE);\n    int numberOfEntries = stsd.readInt();\n    StsdData out = new StsdData(numberOfEntries);\n    for (int i = 0; i < numberOfEntries; i++) {\n      int childStartPosition = stsd.getPosition();\n      int childAtomSize = stsd.readInt();\n      Assertions.checkArgument(childAtomSize > 0, \"childAtomSize should be positive\");\n      int childAtomType = stsd.readInt();\n      if (childAtomType == Atom.TYPE_avc1\n          || childAtomType == Atom.TYPE_avc3\n          || childAtomType == Atom.TYPE_encv\n          || childAtomType == Atom.TYPE_mp4v\n          || childAtomType == Atom.TYPE_hvc1\n          || childAtomType == Atom.TYPE_hev1\n          || childAtomType == Atom.TYPE_s263\n          || childAtomType == Atom.TYPE_vp08\n          || childAtomType == Atom.TYPE_vp09\n          || childAtomType == Atom.TYPE_av01\n          || childAtomType == Atom.TYPE_dvav\n          || childAtomType == Atom.TYPE_dva1\n          || childAtomType == Atom.TYPE_dvhe\n          || childAtomType == Atom.TYPE_dvh1) {\n        parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,\n            rotationDegrees, drmInitData, out, i);\n      } else if (childAtomType == Atom.TYPE_mp4a\n          || childAtomType == Atom.TYPE_enca\n          || childAtomType == Atom.TYPE_ac_3\n          || childAtomType == Atom.TYPE_ec_3\n          || childAtomType == Atom.TYPE_ac_4\n          || childAtomType == Atom.TYPE_dtsc\n          || childAtomType == Atom.TYPE_dtse\n          || childAtomType == Atom.TYPE_dtsh\n          || childAtomType == Atom.TYPE_dtsl\n          || childAtomType == Atom.TYPE_samr\n          || childAtomType == Atom.TYPE_sawb\n          || childAtomType == Atom.TYPE_lpcm\n          || childAtomType == Atom.TYPE_sowt\n          || childAtomType == Atom.TYPE__mp3\n          || childAtomType == Atom.TYPE_alac\n          || childAtomType == Atom.TYPE_alaw\n          || childAtomType == Atom.TYPE_ulaw\n          || childAtomType == Atom.TYPE_Opus\n          || childAtomType == Atom.TYPE_fLaC) {\n        parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,\n            language, isQuickTime, drmInitData, out, i);\n      } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g\n          || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp\n          || childAtomType == Atom.TYPE_c608) {\n        parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,\n            language, out);\n      } else if (childAtomType == Atom.TYPE_camm) {\n        out.format = Format.createSampleFormat(Integer.toString(trackId),\n            MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null);\n      }\n      stsd.setPosition(childStartPosition + childAtomSize);\n    }\n    return out;\n  }\n\n  private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,\n      int atomSize, int trackId, String language, StsdData out) throws ParserException {\n    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);\n\n    // Default values.\n    List<byte[]> initializationData = null;\n    long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;\n\n    String mimeType;\n    if (atomType == Atom.TYPE_TTML) {\n      mimeType = MimeTypes.APPLICATION_TTML;\n    } else if (atomType == Atom.TYPE_tx3g) {\n      mimeType = MimeTypes.APPLICATION_TX3G;\n      int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;\n      byte[] sampleDescriptionData = new byte[sampleDescriptionLength];\n      parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);\n      initializationData = Collections.singletonList(sampleDescriptionData);\n    } else if (atomType == Atom.TYPE_wvtt) {\n      mimeType = MimeTypes.APPLICATION_MP4VTT;\n    } else if (atomType == Atom.TYPE_stpp) {\n      mimeType = MimeTypes.APPLICATION_TTML;\n      subsampleOffsetUs = 0; // Subsample timing is absolute.\n    } else if (atomType == Atom.TYPE_c608) {\n      // Defined by the QuickTime File Format specification.\n      mimeType = MimeTypes.APPLICATION_MP4CEA608;\n      out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;\n    } else {\n      // Never happens.\n      throw new IllegalStateException();\n    }\n\n    out.format =\n        Format.createTextSampleFormat(\n            Integer.toString(trackId),\n            mimeType,\n            /* codecs= */ null,\n            /* bitrate= */ Format.NO_VALUE,\n            /* selectionFlags= */ 0,\n            language,\n            /* accessibilityChannel= */ Format.NO_VALUE,\n            /* drmInitData= */ null,\n            subsampleOffsetUs,\n            initializationData);\n  }\n\n  private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,\n      int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out,\n      int entryIndex) throws ParserException {\n    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);\n\n    parent.skipBytes(16);\n    int width = parent.readUnsignedShort();\n    int height = parent.readUnsignedShort();\n    boolean pixelWidthHeightRatioFromPasp = false;\n    float pixelWidthHeightRatio = 1;\n    parent.skipBytes(50);\n\n    int childPosition = parent.getPosition();\n    if (atomType == Atom.TYPE_encv) {\n      Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData(\n          parent, position, size);\n      if (sampleEntryEncryptionData != null) {\n        atomType = sampleEntryEncryptionData.first;\n        drmInitData = drmInitData == null ? null\n            : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);\n        out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;\n      }\n      parent.setPosition(childPosition);\n    }\n    // TODO: Uncomment when [Internal: b/63092960] is fixed.\n    // else {\n    //   drmInitData = null;\n    // }\n\n    List<byte[]> initializationData = null;\n    String mimeType = null;\n    String codecs = null;\n    byte[] projectionData = null;\n    @C.StereoMode\n    int stereoMode = Format.NO_VALUE;\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childStartPosition = parent.getPosition();\n      int childAtomSize = parent.readInt();\n      if (childAtomSize == 0 && parent.getPosition() - position == size) {\n        // Handle optional terminating four zero bytes in MOV files.\n        break;\n      }\n      Assertions.checkArgument(childAtomSize > 0, \"childAtomSize should be positive\");\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_avcC) {\n        Assertions.checkState(mimeType == null);\n        mimeType = MimeTypes.VIDEO_H264;\n        parent.setPosition(childStartPosition + Atom.HEADER_SIZE);\n        AvcConfig avcConfig = AvcConfig.parse(parent);\n        initializationData = avcConfig.initializationData;\n        out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;\n        if (!pixelWidthHeightRatioFromPasp) {\n          pixelWidthHeightRatio = avcConfig.pixelWidthAspectRatio;\n        }\n      } else if (childAtomType == Atom.TYPE_hvcC) {\n        Assertions.checkState(mimeType == null);\n        mimeType = MimeTypes.VIDEO_H265;\n        parent.setPosition(childStartPosition + Atom.HEADER_SIZE);\n        HevcConfig hevcConfig = HevcConfig.parse(parent);\n        initializationData = hevcConfig.initializationData;\n        out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;\n      } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) {\n        DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent);\n        if (dolbyVisionConfig != null) {\n          codecs = dolbyVisionConfig.codecs;\n          mimeType = MimeTypes.VIDEO_DOLBY_VISION;\n        }\n      } else if (childAtomType == Atom.TYPE_vpcC) {\n        Assertions.checkState(mimeType == null);\n        mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9;\n      } else if (childAtomType == Atom.TYPE_av1C) {\n        Assertions.checkState(mimeType == null);\n        mimeType = MimeTypes.VIDEO_AV1;\n      } else if (childAtomType == Atom.TYPE_d263) {\n        Assertions.checkState(mimeType == null);\n        mimeType = MimeTypes.VIDEO_H263;\n      } else if (childAtomType == Atom.TYPE_esds) {\n        Assertions.checkState(mimeType == null);\n        Pair<String, byte[]> mimeTypeAndInitializationData =\n            parseEsdsFromParent(parent, childStartPosition);\n        mimeType = mimeTypeAndInitializationData.first;\n        initializationData = Collections.singletonList(mimeTypeAndInitializationData.second);\n      } else if (childAtomType == Atom.TYPE_pasp) {\n        pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);\n        pixelWidthHeightRatioFromPasp = true;\n      } else if (childAtomType == Atom.TYPE_sv3d) {\n        projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize);\n      } else if (childAtomType == Atom.TYPE_st3d) {\n        int version = parent.readUnsignedByte();\n        parent.skipBytes(3); // Flags.\n        if (version == 0) {\n          int layout = parent.readUnsignedByte();\n          switch (layout) {\n            case 0:\n              stereoMode = C.STEREO_MODE_MONO;\n              break;\n            case 1:\n              stereoMode = C.STEREO_MODE_TOP_BOTTOM;\n              break;\n            case 2:\n              stereoMode = C.STEREO_MODE_LEFT_RIGHT;\n              break;\n            case 3:\n              stereoMode = C.STEREO_MODE_STEREO_MESH;\n              break;\n            default:\n              break;\n          }\n        }\n      }\n      childPosition += childAtomSize;\n    }\n\n    // If the media type was not recognized, ignore the track.\n    if (mimeType == null) {\n      return;\n    }\n\n    out.format =\n        Format.createVideoSampleFormat(\n            Integer.toString(trackId),\n            mimeType,\n            codecs,\n            /* bitrate= */ Format.NO_VALUE,\n            /* maxInputSize= */ Format.NO_VALUE,\n            width,\n            height,\n            /* frameRate= */ Format.NO_VALUE,\n            initializationData,\n            rotationDegrees,\n            pixelWidthHeightRatio,\n            projectionData,\n            stereoMode,\n            /* colorInfo= */ null,\n            drmInitData);\n  }\n\n  /**\n   * Parses the edts atom (defined in 14496-12 subsection 8.6.5).\n   *\n   * @param edtsAtom edts (edit box) atom to decode.\n   * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are\n   *     not present.\n   */\n  private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {\n    Atom.LeafAtom elst;\n    if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) {\n      return Pair.create(null, null);\n    }\n    ParsableByteArray elstData = elst.data;\n    elstData.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = elstData.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    int entryCount = elstData.readUnsignedIntToInt();\n    long[] editListDurations = new long[entryCount];\n    long[] editListMediaTimes = new long[entryCount];\n    for (int i = 0; i < entryCount; i++) {\n      editListDurations[i] =\n          version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt();\n      editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt();\n      int mediaRateInteger = elstData.readShort();\n      if (mediaRateInteger != 1) {\n        // The extractor does not handle dwell edits (mediaRateInteger == 0).\n        throw new IllegalArgumentException(\"Unsupported media rate.\");\n      }\n      elstData.skipBytes(2);\n    }\n    return Pair.create(editListDurations, editListMediaTimes);\n  }\n\n  private static float parsePaspFromParent(ParsableByteArray parent, int position) {\n    parent.setPosition(position + Atom.HEADER_SIZE);\n    int hSpacing = parent.readUnsignedIntToInt();\n    int vSpacing = parent.readUnsignedIntToInt();\n    return (float) hSpacing / vSpacing;\n  }\n\n  private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,\n      int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,\n      StsdData out, int entryIndex) throws ParserException {\n    parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);\n\n    int quickTimeSoundDescriptionVersion = 0;\n    if (isQuickTime) {\n      quickTimeSoundDescriptionVersion = parent.readUnsignedShort();\n      parent.skipBytes(6);\n    } else {\n      parent.skipBytes(8);\n    }\n\n    int channelCount;\n    int sampleRate;\n\n    if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {\n      channelCount = parent.readUnsignedShort();\n      parent.skipBytes(6);  // sampleSize, compressionId, packetSize.\n      sampleRate = parent.readUnsignedFixedPoint1616();\n\n      if (quickTimeSoundDescriptionVersion == 1) {\n        parent.skipBytes(16);\n      }\n    } else if (quickTimeSoundDescriptionVersion == 2) {\n      parent.skipBytes(16);  // always[3,16,Minus2,0,65536], sizeOfStructOnly\n\n      sampleRate = (int) Math.round(parent.readDouble());\n      channelCount = parent.readUnsignedIntToInt();\n\n      // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket,\n      // constLPCMFramesPerAudioPacket.\n      parent.skipBytes(20);\n    } else {\n      // Unsupported version.\n      return;\n    }\n\n    int childPosition = parent.getPosition();\n    if (atomType == Atom.TYPE_enca) {\n      Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData(\n          parent, position, size);\n      if (sampleEntryEncryptionData != null) {\n        atomType = sampleEntryEncryptionData.first;\n        drmInitData = drmInitData == null ? null\n            : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);\n        out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;\n      }\n      parent.setPosition(childPosition);\n    }\n    // TODO: Uncomment when [Internal: b/63092960] is fixed.\n    // else {\n    //   drmInitData = null;\n    // }\n\n    // If the atom type determines a MIME type, set it immediately.\n    String mimeType = null;\n    if (atomType == Atom.TYPE_ac_3) {\n      mimeType = MimeTypes.AUDIO_AC3;\n    } else if (atomType == Atom.TYPE_ec_3) {\n      mimeType = MimeTypes.AUDIO_E_AC3;\n    } else if (atomType == Atom.TYPE_ac_4) {\n      mimeType = MimeTypes.AUDIO_AC4;\n    } else if (atomType == Atom.TYPE_dtsc) {\n      mimeType = MimeTypes.AUDIO_DTS;\n    } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) {\n      mimeType = MimeTypes.AUDIO_DTS_HD;\n    } else if (atomType == Atom.TYPE_dtse) {\n      mimeType = MimeTypes.AUDIO_DTS_EXPRESS;\n    } else if (atomType == Atom.TYPE_samr) {\n      mimeType = MimeTypes.AUDIO_AMR_NB;\n    } else if (atomType == Atom.TYPE_sawb) {\n      mimeType = MimeTypes.AUDIO_AMR_WB;\n    } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {\n      mimeType = MimeTypes.AUDIO_RAW;\n    } else if (atomType == Atom.TYPE__mp3) {\n      mimeType = MimeTypes.AUDIO_MPEG;\n    } else if (atomType == Atom.TYPE_alac) {\n      mimeType = MimeTypes.AUDIO_ALAC;\n    } else if (atomType == Atom.TYPE_alaw) {\n      mimeType = MimeTypes.AUDIO_ALAW;\n    } else if (atomType == Atom.TYPE_ulaw) {\n      mimeType = MimeTypes.AUDIO_MLAW;\n    } else if (atomType == Atom.TYPE_Opus) {\n      mimeType = MimeTypes.AUDIO_OPUS;\n    } else if (atomType == Atom.TYPE_fLaC) {\n      mimeType = MimeTypes.AUDIO_FLAC;\n    }\n\n    byte[] initializationData = null;\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childAtomSize = parent.readInt();\n      Assertions.checkArgument(childAtomSize > 0, \"childAtomSize should be positive\");\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) {\n        int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition\n            : findEsdsPosition(parent, childPosition, childAtomSize);\n        if (esdsAtomPosition != C.POSITION_UNSET) {\n          Pair<String, byte[]> mimeTypeAndInitializationData =\n              parseEsdsFromParent(parent, esdsAtomPosition);\n          mimeType = mimeTypeAndInitializationData.first;\n          initializationData = mimeTypeAndInitializationData.second;\n          if (MimeTypes.AUDIO_AAC.equals(mimeType)) {\n            // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,\n            // which is more reliable. See [Internal: b/10903778].\n            Pair<Integer, Integer> audioSpecificConfig =\n                CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData);\n            sampleRate = audioSpecificConfig.first;\n            channelCount = audioSpecificConfig.second;\n          }\n        }\n      } else if (childAtomType == Atom.TYPE_dac3) {\n        parent.setPosition(Atom.HEADER_SIZE + childPosition);\n        out.format = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language,\n            drmInitData);\n      } else if (childAtomType == Atom.TYPE_dec3) {\n        parent.setPosition(Atom.HEADER_SIZE + childPosition);\n        out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language,\n            drmInitData);\n      } else if (childAtomType == Atom.TYPE_dac4) {\n        parent.setPosition(Atom.HEADER_SIZE + childPosition);\n        out.format =\n            Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData);\n      } else if (childAtomType == Atom.TYPE_ddts) {\n        out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,\n            Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,\n            language);\n      } else if (childAtomType == Atom.TYPE_dOps) {\n        // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic\n        // Signature and the body of the dOps atom.\n        int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE;\n        initializationData = new byte[opusMagic.length + childAtomBodySize];\n        System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);\n        parent.setPosition(childPosition + Atom.HEADER_SIZE);\n        parent.readBytes(initializationData, opusMagic.length, childAtomBodySize);\n      } else if (childAtomType == Atom.TYPE_dfLa) {\n        int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;\n        initializationData = new byte[4 + childAtomBodySize];\n        initializationData[0] = 0x66; // f\n        initializationData[1] = 0x4C; // L\n        initializationData[2] = 0x61; // a\n        initializationData[3] = 0x43; // C\n        parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);\n        parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize);\n      } else if (childAtomType == Atom.TYPE_alac) {\n        int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;\n        initializationData = new byte[childAtomBodySize];\n        parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);\n        parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize);\n        // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,\n        // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629.\n        Pair<Integer, Integer> audioSpecificConfig =\n            CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData);\n        sampleRate = audioSpecificConfig.first;\n        channelCount = audioSpecificConfig.second;\n      }\n      childPosition += childAtomSize;\n    }\n\n    if (out.format == null && mimeType != null) {\n      // TODO: Determine the correct PCM encoding.\n      @C.PcmEncoding int pcmEncoding =\n          MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;\n      out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,\n          Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,\n          initializationData == null ? null : Collections.singletonList(initializationData),\n          drmInitData, 0, language);\n    }\n  }\n\n  /**\n   * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds\n   * box is found\n   */\n  private static int findEsdsPosition(ParsableByteArray parent, int position, int size) {\n    int childAtomPosition = parent.getPosition();\n    while (childAtomPosition - position < size) {\n      parent.setPosition(childAtomPosition);\n      int childAtomSize = parent.readInt();\n      Assertions.checkArgument(childAtomSize > 0, \"childAtomSize should be positive\");\n      int childType = parent.readInt();\n      if (childType == Atom.TYPE_esds) {\n        return childAtomPosition;\n      }\n      childAtomPosition += childAtomSize;\n    }\n    return C.POSITION_UNSET;\n  }\n\n  /**\n   * Returns codec-specific initialization data contained in an esds box.\n   */\n  private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) {\n    parent.setPosition(position + Atom.HEADER_SIZE + 4);\n    // Start of the ES_Descriptor (defined in 14496-1)\n    parent.skipBytes(1); // ES_Descriptor tag\n    parseExpandableClassSize(parent);\n    parent.skipBytes(2); // ES_ID\n\n    int flags = parent.readUnsignedByte();\n    if ((flags & 0x80 /* streamDependenceFlag */) != 0) {\n      parent.skipBytes(2);\n    }\n    if ((flags & 0x40 /* URL_Flag */) != 0) {\n      parent.skipBytes(parent.readUnsignedShort());\n    }\n    if ((flags & 0x20 /* OCRstreamFlag */) != 0) {\n      parent.skipBytes(2);\n    }\n\n    // Start of the DecoderConfigDescriptor (defined in 14496-1)\n    parent.skipBytes(1); // DecoderConfigDescriptor tag\n    parseExpandableClassSize(parent);\n\n    // Set the MIME type based on the object type indication (14496-1 table 5).\n    int objectTypeIndication = parent.readUnsignedByte();\n    String mimeType = getMimeTypeFromMp4ObjectType(objectTypeIndication);\n    if (MimeTypes.AUDIO_MPEG.equals(mimeType)\n        || MimeTypes.AUDIO_DTS.equals(mimeType)\n        || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) {\n      return Pair.create(mimeType, null);\n    }\n\n    parent.skipBytes(12);\n\n    // Start of the DecoderSpecificInfo.\n    parent.skipBytes(1); // DecoderSpecificInfo tag\n    int initializationDataSize = parseExpandableClassSize(parent);\n    byte[] initializationData = new byte[initializationDataSize];\n    parent.readBytes(initializationData, 0, initializationDataSize);\n    return Pair.create(mimeType, initializationData);\n  }\n\n  /**\n   * Parses encryption data from an audio/video sample entry, returning a pair consisting of the\n   * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common\n   * encryption sinf atom was present.\n   */\n  private static Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData(\n      ParsableByteArray parent, int position, int size) {\n    int childPosition = parent.getPosition();\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childAtomSize = parent.readInt();\n      Assertions.checkArgument(childAtomSize > 0, \"childAtomSize should be positive\");\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_sinf) {\n        Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent,\n            childPosition, childAtomSize);\n        if (result != null) {\n          return result;\n        }\n      }\n      childPosition += childAtomSize;\n    }\n    return null;\n  }\n\n  /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent(\n      ParsableByteArray parent, int position, int size) {\n    int childPosition = position + Atom.HEADER_SIZE;\n    int schemeInformationBoxPosition = C.POSITION_UNSET;\n    int schemeInformationBoxSize = 0;\n    String schemeType = null;\n    Integer dataFormat = null;\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childAtomSize = parent.readInt();\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_frma) {\n        dataFormat = parent.readInt();\n      } else if (childAtomType == Atom.TYPE_schm) {\n        parent.skipBytes(4);\n        // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1.\n        schemeType = parent.readString(4);\n      } else if (childAtomType == Atom.TYPE_schi) {\n        schemeInformationBoxPosition = childPosition;\n        schemeInformationBoxSize = childAtomSize;\n      }\n      childPosition += childAtomSize;\n    }\n\n    if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType)\n        || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) {\n      Assertions.checkArgument(dataFormat != null, \"frma atom is mandatory\");\n      Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET,\n          \"schi atom is mandatory\");\n      TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition,\n          schemeInformationBoxSize, schemeType);\n      Assertions.checkArgument(encryptionBox != null, \"tenc atom is mandatory\");\n      return Pair.create(dataFormat, encryptionBox);\n    } else {\n      return null;\n    }\n  }\n\n  private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,\n      int size, String schemeType) {\n    int childPosition = position + Atom.HEADER_SIZE;\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childAtomSize = parent.readInt();\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_tenc) {\n        int fullAtom = parent.readInt();\n        int version = Atom.parseFullAtomVersion(fullAtom);\n        parent.skipBytes(1); // reserved = 0.\n        int defaultCryptByteBlock = 0;\n        int defaultSkipByteBlock = 0;\n        if (version == 0) {\n          parent.skipBytes(1); // reserved = 0.\n        } else /* version 1 or greater */ {\n          int patternByte = parent.readUnsignedByte();\n          defaultCryptByteBlock = (patternByte & 0xF0) >> 4;\n          defaultSkipByteBlock = patternByte & 0x0F;\n        }\n        boolean defaultIsProtected = parent.readUnsignedByte() == 1;\n        int defaultPerSampleIvSize = parent.readUnsignedByte();\n        byte[] defaultKeyId = new byte[16];\n        parent.readBytes(defaultKeyId, 0, defaultKeyId.length);\n        byte[] constantIv = null;\n        if (defaultIsProtected && defaultPerSampleIvSize == 0) {\n          int constantIvSize = parent.readUnsignedByte();\n          constantIv = new byte[constantIvSize];\n          parent.readBytes(constantIv, 0, constantIvSize);\n        }\n        return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize,\n            defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv);\n      }\n      childPosition += childAtomSize;\n    }\n    return null;\n  }\n\n  /**\n   * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media.\n   */\n  private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) {\n    int childPosition = position + Atom.HEADER_SIZE;\n    while (childPosition - position < size) {\n      parent.setPosition(childPosition);\n      int childAtomSize = parent.readInt();\n      int childAtomType = parent.readInt();\n      if (childAtomType == Atom.TYPE_proj) {\n        return Arrays.copyOfRange(parent.data, childPosition, childPosition + childAtomSize);\n      }\n      childPosition += childAtomSize;\n    }\n    return null;\n  }\n\n  /**\n   * Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3.\n   */\n  private static int parseExpandableClassSize(ParsableByteArray data) {\n    int currentByte = data.readUnsignedByte();\n    int size = currentByte & 0x7F;\n    while ((currentByte & 0x80) == 0x80) {\n      currentByte = data.readUnsignedByte();\n      size = (size << 7) | (currentByte & 0x7F);\n    }\n    return size;\n  }\n\n  /** Returns whether it's possible to apply the specified edit using gapless playback info. */\n  private static boolean canApplyEditWithGaplessInfo(\n      long[] timestamps, long duration, long editStartTime, long editEndTime) {\n    int lastIndex = timestamps.length - 1;\n    int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);\n    int earliestPaddingIndex =\n        Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);\n    return timestamps[0] <= editStartTime\n        && editStartTime < timestamps[latestDelayIndex]\n        && timestamps[earliestPaddingIndex] < editEndTime\n        && editEndTime <= duration;\n  }\n\n  private AtomParsers() {\n    // Prevent instantiation.\n  }\n\n  private static final class ChunkIterator {\n\n    public final int length;\n\n    public int index;\n    public int numSamples;\n    public long offset;\n\n    private final boolean chunkOffsetsAreLongs;\n    private final ParsableByteArray chunkOffsets;\n    private final ParsableByteArray stsc;\n\n    private int nextSamplesPerChunkChangeIndex;\n    private int remainingSamplesPerChunkChanges;\n\n    public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets,\n        boolean chunkOffsetsAreLongs) {\n      this.stsc = stsc;\n      this.chunkOffsets = chunkOffsets;\n      this.chunkOffsetsAreLongs = chunkOffsetsAreLongs;\n      chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);\n      length = chunkOffsets.readUnsignedIntToInt();\n      stsc.setPosition(Atom.FULL_HEADER_SIZE);\n      remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();\n      Assertions.checkState(stsc.readInt() == 1, \"first_chunk must be 1\");\n      index = -1;\n    }\n\n    public boolean moveNext() {\n      if (++index == length) {\n        return false;\n      }\n      offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong()\n          : chunkOffsets.readUnsignedInt();\n      if (index == nextSamplesPerChunkChangeIndex) {\n        numSamples = stsc.readUnsignedIntToInt();\n        stsc.skipBytes(4); // Skip sample_description_index\n        nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0\n            ? (stsc.readUnsignedIntToInt() - 1) : C.INDEX_UNSET;\n      }\n      return true;\n    }\n\n  }\n\n  /**\n   * Holds data parsed from a tkhd atom.\n   */\n  private static final class TkhdData {\n\n    private final int id;\n    private final long duration;\n    private final int rotationDegrees;\n\n    public TkhdData(int id, long duration, int rotationDegrees) {\n      this.id = id;\n      this.duration = duration;\n      this.rotationDegrees = rotationDegrees;\n    }\n\n  }\n\n  /**\n   * Holds data parsed from an stsd atom and its children.\n   */\n  private static final class StsdData {\n\n    public static final int STSD_HEADER_SIZE = 8;\n\n    public final TrackEncryptionBox[] trackEncryptionBoxes;\n\n    public Format format;\n    public int nalUnitLengthFieldLength;\n    @Track.Transformation\n    public int requiredSampleTransformation;\n\n    public StsdData(int numberOfEntries) {\n      trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];\n      requiredSampleTransformation = Track.TRANSFORMATION_NONE;\n    }\n\n  }\n\n  /**\n   * A box containing sample sizes (e.g. stsz, stz2).\n   */\n  private interface SampleSizeBox {\n\n    /**\n     * Returns the number of samples.\n     */\n    int getSampleCount();\n\n    /**\n     * Returns the size for the next sample.\n     */\n    int readNextSampleSize();\n\n    /**\n     * Returns whether samples have a fixed size.\n     */\n    boolean isFixedSampleSize();\n\n  }\n\n  /**\n   * An stsz sample size box.\n   */\n  /* package */ static final class StszSampleSizeBox implements SampleSizeBox {\n\n    private final int fixedSampleSize;\n    private final int sampleCount;\n    private final ParsableByteArray data;\n\n    public StszSampleSizeBox(Atom.LeafAtom stszAtom) {\n      data = stszAtom.data;\n      data.setPosition(Atom.FULL_HEADER_SIZE);\n      fixedSampleSize = data.readUnsignedIntToInt();\n      sampleCount = data.readUnsignedIntToInt();\n    }\n\n    @Override\n    public int getSampleCount() {\n      return sampleCount;\n    }\n\n    @Override\n    public int readNextSampleSize() {\n      return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize;\n    }\n\n    @Override\n    public boolean isFixedSampleSize() {\n      return fixedSampleSize != 0;\n    }\n\n  }\n\n  /**\n   * An stz2 sample size box.\n   */\n  /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox {\n\n    private final ParsableByteArray data;\n    private final int sampleCount;\n    private final int fieldSize; // Can be 4, 8, or 16.\n\n    // Used only if fieldSize == 4.\n    private int sampleIndex;\n    private int currentByte;\n\n    public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) {\n      data = stz2Atom.data;\n      data.setPosition(Atom.FULL_HEADER_SIZE);\n      fieldSize = data.readUnsignedIntToInt() & 0x000000FF;\n      sampleCount = data.readUnsignedIntToInt();\n    }\n\n    @Override\n    public int getSampleCount() {\n      return sampleCount;\n    }\n\n    @Override\n    public int readNextSampleSize() {\n      if (fieldSize == 8) {\n        return data.readUnsignedByte();\n      } else if (fieldSize == 16) {\n        return data.readUnsignedShort();\n      } else {\n        // fieldSize == 4.\n        if ((sampleIndex++ % 2) == 0) {\n          // Read the next byte into our cached byte when we are reading the upper bits.\n          currentByte = data.readUnsignedByte();\n          // Read the upper bits from the byte and shift them to the lower 4 bits.\n          return (currentByte & 0xF0) >> 4;\n        } else {\n          // Mask out the upper 4 bits of the last byte we read.\n          return currentByte & 0x0F;\n        }\n      }\n    }\n\n    @Override\n    public boolean isFixedSampleSize() {\n      return false;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\n/* package */ final class DefaultSampleValues {\n\n  public final int sampleDescriptionIndex;\n  public final int duration;\n  public final int size;\n  public final int flags;\n\n  public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {\n    this.sampleDescriptionIndex = sampleDescriptionIndex;\n    this.duration = duration;\n    this.size = size;\n    this.flags = flags;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio).\n */\n/* package */ final class FixedSampleSizeRechunker {\n\n  /**\n   * The result of a rechunking operation.\n   */\n  public static final class Results {\n\n    public final long[] offsets;\n    public final int[] sizes;\n    public final int maximumSize;\n    public final long[] timestamps;\n    public final int[] flags;\n    public final long duration;\n\n    private Results(\n        long[] offsets,\n        int[] sizes,\n        int maximumSize,\n        long[] timestamps,\n        int[] flags,\n        long duration) {\n      this.offsets = offsets;\n      this.sizes = sizes;\n      this.maximumSize = maximumSize;\n      this.timestamps = timestamps;\n      this.flags = flags;\n      this.duration = duration;\n    }\n\n  }\n\n  /**\n   * Maximum number of bytes for each buffer in rechunked output.\n   */\n  private static final int MAX_SAMPLE_SIZE = 8 * 1024;\n\n  /**\n   * Rechunk the given fixed sample size input to produce a new sequence of samples.\n   *\n   * @param fixedSampleSize Size in bytes of each sample.\n   * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk.\n   * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks.\n   * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units.\n   */\n  public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts,\n      long timestampDeltaInTimeUnits) {\n    int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize;\n\n    // Count the number of new, rechunked buffers.\n    int rechunkedSampleCount = 0;\n    for (int chunkSampleCount : chunkSampleCounts) {\n      rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount);\n    }\n\n    long[] offsets = new long[rechunkedSampleCount];\n    int[] sizes = new int[rechunkedSampleCount];\n    int maximumSize = 0;\n    long[] timestamps = new long[rechunkedSampleCount];\n    int[] flags = new int[rechunkedSampleCount];\n\n    int originalSampleIndex = 0;\n    int newSampleIndex = 0;\n    for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) {\n      int chunkSamplesRemaining = chunkSampleCounts[chunkIndex];\n      long sampleOffset = chunkOffsets[chunkIndex];\n\n      while (chunkSamplesRemaining > 0) {\n        int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining);\n\n        offsets[newSampleIndex] = sampleOffset;\n        sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount;\n        maximumSize = Math.max(maximumSize, sizes[newSampleIndex]);\n        timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex);\n        flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME;\n\n        sampleOffset += sizes[newSampleIndex];\n        originalSampleIndex += bufferSampleCount;\n\n        chunkSamplesRemaining -= bufferSampleCount;\n        newSampleIndex++;\n      }\n    }\n    long duration = timestampDeltaInTimeUnits * originalSampleIndex;\n\n    return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);\n  }\n\n  private FixedSampleSizeRechunker() {\n    // Prevent instantiation.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport android.util.Pair;\nimport android.util.SparseArray;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.Ac4Util;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;\nimport com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;\nimport com.google.android.exoplayer2.text.cea.CeaUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n/** Extracts data from the FMP4 container format. */\n@SuppressWarnings(\"ConstantField\")\npublic class FragmentedMp4Extractor implements Extractor {\n\n  /** Factory for {@link FragmentedMp4Extractor} instances. */\n  public static final ExtractorsFactory FACTORY =\n      () -> new Extractor[] {new FragmentedMp4Extractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag values are {@link\n   * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX},\n   * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link\n   * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,\n        FLAG_WORKAROUND_IGNORE_TFDT_BOX,\n        FLAG_ENABLE_EMSG_TRACK,\n        FLAG_SIDELOADED,\n        FLAG_WORKAROUND_IGNORE_EDIT_LISTS\n      })\n  public @interface Flags {}\n  /**\n   * Flag to work around an issue in some video streams where every frame is marked as a sync frame.\n   * The workaround overrides the sync frame flags in the stream, forcing them to false except for\n   * the first sample in each segment.\n   * <p>\n   * This flag does nothing if the stream is not a video stream.\n   */\n  public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;\n  /** Flag to ignore any tfdt boxes in the stream. */\n  public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2\n  /**\n   * Flag to indicate that the extractor should output an event message metadata track. Any event\n   * messages in the stream will be delivered as samples to this track.\n   */\n  public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4\n  /**\n   * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4\n   * container.\n   */\n  private static final int FLAG_SIDELOADED = 1 << 3; // 8\n  /** Flag to ignore any edit lists in the stream. */\n  public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16\n\n  private static final String TAG = \"FragmentedMp4Extractor\";\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967;\n\n  private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =\n      new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};\n  private static final Format EMSG_FORMAT =\n      Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);\n\n  // Parser states.\n  private static final int STATE_READING_ATOM_HEADER = 0;\n  private static final int STATE_READING_ATOM_PAYLOAD = 1;\n  private static final int STATE_READING_ENCRYPTION_DATA = 2;\n  private static final int STATE_READING_SAMPLE_START = 3;\n  private static final int STATE_READING_SAMPLE_CONTINUE = 4;\n\n  // Workarounds.\n  @Flags private final int flags;\n  @Nullable private final Track sideloadedTrack;\n\n  // Sideloaded data.\n  private final List<Format> closedCaptionFormats;\n  @Nullable private final DrmInitData sideloadedDrmInitData;\n\n  // Track-linked data bundle, accessible as a whole through trackID.\n  private final SparseArray<TrackBundle> trackBundles;\n\n  // Temporary arrays.\n  private final ParsableByteArray nalStartCode;\n  private final ParsableByteArray nalPrefix;\n  private final ParsableByteArray nalBuffer;\n  private final byte[] scratchBytes;\n  private final ParsableByteArray scratch;\n\n  // Adjusts sample timestamps.\n  @Nullable private final TimestampAdjuster timestampAdjuster;\n\n  private final EventMessageEncoder eventMessageEncoder;\n\n  // Parser state.\n  private final ParsableByteArray atomHeader;\n  private final ArrayDeque<ContainerAtom> containerAtoms;\n  private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;\n  @Nullable private final TrackOutput additionalEmsgTrackOutput;\n\n  private int parserState;\n  private int atomType;\n  private long atomSize;\n  private int atomHeaderBytesRead;\n  private ParsableByteArray atomData;\n  private long endOfMdatPosition;\n  private int pendingMetadataSampleBytes;\n  private long pendingSeekTimeUs;\n\n  private long durationUs;\n  private long segmentIndexEarliestPresentationTimeUs;\n  private TrackBundle currentTrackBundle;\n  private int sampleSize;\n  private int sampleBytesWritten;\n  private int sampleCurrentNalBytesRemaining;\n  private boolean processSeiNalUnitPayload;\n  private boolean isAc4HeaderRequired;\n\n  // Extractor output.\n  private ExtractorOutput extractorOutput;\n  private TrackOutput[] emsgTrackOutputs;\n  private TrackOutput[] cea608TrackOutputs;\n\n  // Whether extractorOutput.seekMap has been called.\n  private boolean haveOutputSeekMap;\n\n  public FragmentedMp4Extractor() {\n    this(0);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public FragmentedMp4Extractor(@Flags int flags) {\n    this(flags, null);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.\n   */\n  public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) {\n    this(flags, timestampAdjuster, null, null);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.\n   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not\n   *     receive a moov box in the input data. Null if a moov box is expected.\n   * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the\n   *     pssh boxes (if present) will be used.\n   */\n  public FragmentedMp4Extractor(\n      @Flags int flags,\n      @Nullable TimestampAdjuster timestampAdjuster,\n      @Nullable Track sideloadedTrack,\n      @Nullable DrmInitData sideloadedDrmInitData) {\n    this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList());\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.\n   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not\n   *     receive a moov box in the input data. Null if a moov box is expected.\n   * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the\n   *     pssh boxes (if present) will be used.\n   * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed\n   *     caption channels to expose.\n   */\n  public FragmentedMp4Extractor(\n      @Flags int flags,\n      @Nullable TimestampAdjuster timestampAdjuster,\n      @Nullable Track sideloadedTrack,\n      @Nullable DrmInitData sideloadedDrmInitData,\n      List<Format> closedCaptionFormats) {\n    this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,\n        closedCaptionFormats, null);\n  }\n\n  /**\n   * @param flags Flags that control the extractor's behavior.\n   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.\n   * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not\n   *     receive a moov box in the input data. Null if a moov box is expected.\n   * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the\n   *     pssh boxes (if present) will be used.\n   * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed\n   *     caption channels to expose.\n   * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages\n   *     targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special\n   *     handling of emsg messages for players is not required.\n   */\n  public FragmentedMp4Extractor(\n      @Flags int flags,\n      @Nullable TimestampAdjuster timestampAdjuster,\n      @Nullable Track sideloadedTrack,\n      @Nullable DrmInitData sideloadedDrmInitData,\n      List<Format> closedCaptionFormats,\n      @Nullable TrackOutput additionalEmsgTrackOutput) {\n    this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);\n    this.timestampAdjuster = timestampAdjuster;\n    this.sideloadedTrack = sideloadedTrack;\n    this.sideloadedDrmInitData = sideloadedDrmInitData;\n    this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);\n    this.additionalEmsgTrackOutput = additionalEmsgTrackOutput;\n    eventMessageEncoder = new EventMessageEncoder();\n    atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);\n    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);\n    nalPrefix = new ParsableByteArray(5);\n    nalBuffer = new ParsableByteArray();\n    scratchBytes = new byte[16];\n    scratch = new ParsableByteArray(scratchBytes);\n    containerAtoms = new ArrayDeque<>();\n    pendingMetadataSampleInfos = new ArrayDeque<>();\n    trackBundles = new SparseArray<>();\n    durationUs = C.TIME_UNSET;\n    pendingSeekTimeUs = C.TIME_UNSET;\n    segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;\n    enterReadingAtomHeaderState();\n  }\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return Sniffer.sniffFragmented(input);\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    extractorOutput = output;\n    if (sideloadedTrack != null) {\n      TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type));\n      bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));\n      trackBundles.put(0, bundle);\n      maybeInitExtraTracks();\n      extractorOutput.endTracks();\n    }\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    int trackCount = trackBundles.size();\n    for (int i = 0; i < trackCount; i++) {\n      trackBundles.valueAt(i).reset();\n    }\n    pendingMetadataSampleInfos.clear();\n    pendingMetadataSampleBytes = 0;\n    pendingSeekTimeUs = timeUs;\n    containerAtoms.clear();\n    isAc4HeaderRequired = false;\n    enterReadingAtomHeaderState();\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    while (true) {\n      switch (parserState) {\n        case STATE_READING_ATOM_HEADER:\n          if (!readAtomHeader(input)) {\n            return Extractor.RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_READING_ATOM_PAYLOAD:\n          readAtomPayload(input);\n          break;\n        case STATE_READING_ENCRYPTION_DATA:\n          readEncryptionData(input);\n          break;\n        default:\n          if (readSample(input)) {\n            return RESULT_CONTINUE;\n          }\n      }\n    }\n  }\n\n  private void enterReadingAtomHeaderState() {\n    parserState = STATE_READING_ATOM_HEADER;\n    atomHeaderBytesRead = 0;\n  }\n\n  private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {\n    if (atomHeaderBytesRead == 0) {\n      // Read the standard length atom header.\n      if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {\n        return false;\n      }\n      atomHeaderBytesRead = Atom.HEADER_SIZE;\n      atomHeader.setPosition(0);\n      atomSize = atomHeader.readUnsignedInt();\n      atomType = atomHeader.readInt();\n    }\n\n    if (atomSize == Atom.DEFINES_LARGE_SIZE) {\n      // Read the large size.\n      int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;\n      input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);\n      atomHeaderBytesRead += headerBytesRemaining;\n      atomSize = atomHeader.readUnsignedLongToLong();\n    } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {\n      // The atom extends to the end of the file. Note that if the atom is within a container we can\n      // work out its size even if the input length is unknown.\n      long endPosition = input.getLength();\n      if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) {\n        endPosition = containerAtoms.peek().endPosition;\n      }\n      if (endPosition != C.LENGTH_UNSET) {\n        atomSize = endPosition - input.getPosition() + atomHeaderBytesRead;\n      }\n    }\n\n    if (atomSize < atomHeaderBytesRead) {\n      throw new ParserException(\"Atom size less than header length (unsupported).\");\n    }\n\n    long atomPosition = input.getPosition() - atomHeaderBytesRead;\n    if (atomType == Atom.TYPE_moof) {\n      // The data positions may be updated when parsing the tfhd/trun.\n      int trackCount = trackBundles.size();\n      for (int i = 0; i < trackCount; i++) {\n        TrackFragment fragment = trackBundles.valueAt(i).fragment;\n        fragment.atomPosition = atomPosition;\n        fragment.auxiliaryDataPosition = atomPosition;\n        fragment.dataPosition = atomPosition;\n      }\n    }\n\n    if (atomType == Atom.TYPE_mdat) {\n      currentTrackBundle = null;\n      endOfMdatPosition = atomPosition + atomSize;\n      if (!haveOutputSeekMap) {\n        // This must be the first mdat in the stream.\n        extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition));\n        haveOutputSeekMap = true;\n      }\n      parserState = STATE_READING_ENCRYPTION_DATA;\n      return true;\n    }\n\n    if (shouldParseContainerAtom(atomType)) {\n      long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;\n      containerAtoms.push(new ContainerAtom(atomType, endPosition));\n      if (atomSize == atomHeaderBytesRead) {\n        processAtomEnded(endPosition);\n      } else {\n        // Start reading the first child atom.\n        enterReadingAtomHeaderState();\n      }\n    } else if (shouldParseLeafAtom(atomType)) {\n      if (atomHeaderBytesRead != Atom.HEADER_SIZE) {\n        throw new ParserException(\"Leaf atom defines extended atom size (unsupported).\");\n      }\n      if (atomSize > Integer.MAX_VALUE) {\n        throw new ParserException(\"Leaf atom with length > 2147483647 (unsupported).\");\n      }\n      atomData = new ParsableByteArray((int) atomSize);\n      System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);\n      parserState = STATE_READING_ATOM_PAYLOAD;\n    } else {\n      if (atomSize > Integer.MAX_VALUE) {\n        throw new ParserException(\"Skipping atom with length > 2147483647 (unsupported).\");\n      }\n      atomData = null;\n      parserState = STATE_READING_ATOM_PAYLOAD;\n    }\n\n    return true;\n  }\n\n  private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {\n    int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;\n    if (atomData != null) {\n      input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize);\n      onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());\n    } else {\n      input.skipFully(atomPayloadSize);\n    }\n    processAtomEnded(input.getPosition());\n  }\n\n  private void processAtomEnded(long atomEndPosition) throws ParserException {\n    while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {\n      onContainerAtomRead(containerAtoms.pop());\n    }\n    enterReadingAtomHeaderState();\n  }\n\n  private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException {\n    if (!containerAtoms.isEmpty()) {\n      containerAtoms.peek().add(leaf);\n    } else if (leaf.type == Atom.TYPE_sidx) {\n      Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);\n      segmentIndexEarliestPresentationTimeUs = result.first;\n      extractorOutput.seekMap(result.second);\n      haveOutputSeekMap = true;\n    } else if (leaf.type == Atom.TYPE_emsg) {\n      onEmsgLeafAtomRead(leaf.data);\n    }\n  }\n\n  private void onContainerAtomRead(ContainerAtom container) throws ParserException {\n    if (container.type == Atom.TYPE_moov) {\n      onMoovContainerAtomRead(container);\n    } else if (container.type == Atom.TYPE_moof) {\n      onMoofContainerAtomRead(container);\n    } else if (!containerAtoms.isEmpty()) {\n      containerAtoms.peek().add(container);\n    }\n  }\n\n  private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {\n    Assertions.checkState(sideloadedTrack == null, \"Unexpected moov box.\");\n\n    DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData\n        : getDrmInitDataFromAtoms(moov.leafChildren);\n\n    // Read declaration of track fragments in the Moov box.\n    ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);\n    SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();\n    long duration = C.TIME_UNSET;\n    int mvexChildrenSize = mvex.leafChildren.size();\n    for (int i = 0; i < mvexChildrenSize; i++) {\n      LeafAtom atom = mvex.leafChildren.get(i);\n      if (atom.type == Atom.TYPE_trex) {\n        Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data);\n        defaultSampleValuesArray.put(trexData.first, trexData.second);\n      } else if (atom.type == Atom.TYPE_mehd) {\n        duration = parseMehd(atom.data);\n      }\n    }\n\n    // Construction of tracks.\n    SparseArray<Track> tracks = new SparseArray<>();\n    int moovContainerChildrenSize = moov.containerChildren.size();\n    for (int i = 0; i < moovContainerChildrenSize; i++) {\n      ContainerAtom atom = moov.containerChildren.get(i);\n      if (atom.type == Atom.TYPE_trak) {\n        Track track =\n            modifyTrack(\n                AtomParsers.parseTrak(\n                    atom,\n                    moov.getLeafAtomOfType(Atom.TYPE_mvhd),\n                    duration,\n                    drmInitData,\n                    (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0,\n                    false));\n        if (track != null) {\n          tracks.put(track.id, track);\n        }\n      }\n    }\n\n    int trackCount = tracks.size();\n    if (trackBundles.size() == 0) {\n      // We need to create the track bundles.\n      for (int i = 0; i < trackCount; i++) {\n        Track track = tracks.valueAt(i);\n        TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));\n        trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));\n        trackBundles.put(track.id, trackBundle);\n        durationUs = Math.max(durationUs, track.durationUs);\n      }\n      maybeInitExtraTracks();\n      extractorOutput.endTracks();\n    } else {\n      Assertions.checkState(trackBundles.size() == trackCount);\n      for (int i = 0; i < trackCount; i++) {\n        Track track = tracks.valueAt(i);\n        trackBundles\n            .get(track.id)\n            .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));\n      }\n    }\n  }\n\n  @Nullable\n  protected Track modifyTrack(@Nullable Track track) {\n    return track;\n  }\n\n  private DefaultSampleValues getDefaultSampleValues(\n      SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) {\n    if (defaultSampleValuesArray.size() == 1) {\n      // Ignore track id if there is only one track to cope with non-matching track indices.\n      // See https://github.com/google/ExoPlayer/issues/4477.\n      return defaultSampleValuesArray.valueAt(/* index= */ 0);\n    }\n    return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));\n  }\n\n  private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {\n    parseMoof(moof, trackBundles, flags, scratchBytes);\n    // If drm init data is sideloaded, we ignore pssh boxes.\n    DrmInitData drmInitData = sideloadedDrmInitData != null ? null\n        : getDrmInitDataFromAtoms(moof.leafChildren);\n    if (drmInitData != null) {\n      int trackCount = trackBundles.size();\n      for (int i = 0; i < trackCount; i++) {\n        trackBundles.valueAt(i).updateDrmInitData(drmInitData);\n      }\n    }\n    // If we have a pending seek, advance tracks to their preceding sync frames.\n    if (pendingSeekTimeUs != C.TIME_UNSET) {\n      int trackCount = trackBundles.size();\n      for (int i = 0; i < trackCount; i++) {\n        trackBundles.valueAt(i).seek(pendingSeekTimeUs);\n      }\n      pendingSeekTimeUs = C.TIME_UNSET;\n    }\n  }\n\n  private void maybeInitExtraTracks() {\n    if (emsgTrackOutputs == null) {\n      emsgTrackOutputs = new TrackOutput[2];\n      int emsgTrackOutputCount = 0;\n      if (additionalEmsgTrackOutput != null) {\n        emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput;\n      }\n      if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) {\n        emsgTrackOutputs[emsgTrackOutputCount++] =\n            extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);\n      }\n      emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount);\n\n      for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) {\n        eventMessageTrackOutput.format(EMSG_FORMAT);\n      }\n    }\n    if (cea608TrackOutputs == null) {\n      cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()];\n      for (int i = 0; i < cea608TrackOutputs.length; i++) {\n        TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT);\n        output.format(closedCaptionFormats.get(i));\n        cea608TrackOutputs[i] = output;\n      }\n    }\n  }\n\n  /** Handles an emsg atom (defined in 23009-1). */\n  private void onEmsgLeafAtomRead(ParsableByteArray atom) {\n    if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) {\n      return;\n    }\n    atom.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = atom.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    String schemeIdUri;\n    String value;\n    long timescale;\n    long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0\n    long sampleTimeUs = C.TIME_UNSET;\n    long durationMs;\n    long id;\n    switch (version) {\n      case 0:\n        schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());\n        value = Assertions.checkNotNull(atom.readNullTerminatedString());\n        timescale = atom.readUnsignedInt();\n        presentationTimeDeltaUs =\n            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);\n        if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {\n          sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs;\n        }\n        durationMs =\n            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);\n        id = atom.readUnsignedInt();\n        break;\n      case 1:\n        timescale = atom.readUnsignedInt();\n        sampleTimeUs =\n            Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale);\n        durationMs =\n            Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);\n        id = atom.readUnsignedInt();\n        schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());\n        value = Assertions.checkNotNull(atom.readNullTerminatedString());\n        break;\n      default:\n        Log.w(TAG, \"Skipping unsupported emsg version: \" + version);\n        return;\n    }\n\n    byte[] messageData = new byte[atom.bytesLeft()];\n    atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft());\n    EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData);\n    ParsableByteArray encodedEventMessage =\n        new ParsableByteArray(eventMessageEncoder.encode(eventMessage));\n    int sampleSize = encodedEventMessage.bytesLeft();\n\n    // Output the sample data.\n    for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {\n      encodedEventMessage.setPosition(0);\n      emsgTrackOutput.sampleData(encodedEventMessage, sampleSize);\n    }\n\n    // Output the sample metadata. This is made a little complicated because emsg-v0 atoms\n    // have presentation time *delta* while v1 atoms have absolute presentation time.\n    if (sampleTimeUs == C.TIME_UNSET) {\n      // We need the first sample timestamp in the segment before we can output the metadata.\n      pendingMetadataSampleInfos.addLast(\n          new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));\n      pendingMetadataSampleBytes += sampleSize;\n    } else {\n      if (timestampAdjuster != null) {\n        sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);\n      }\n      for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {\n        emsgTrackOutput.sampleMetadata(\n            sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null);\n      }\n    }\n  }\n\n  /** Parses a trex atom (defined in 14496-12). */\n  private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) {\n    trex.setPosition(Atom.FULL_HEADER_SIZE);\n    int trackId = trex.readInt();\n    int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;\n    int defaultSampleDuration = trex.readUnsignedIntToInt();\n    int defaultSampleSize = trex.readUnsignedIntToInt();\n    int defaultSampleFlags = trex.readInt();\n\n    return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex,\n        defaultSampleDuration, defaultSampleSize, defaultSampleFlags));\n  }\n\n  /**\n   * Parses an mehd atom (defined in 14496-12).\n   */\n  private static long parseMehd(ParsableByteArray mehd) {\n    mehd.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = mehd.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong();\n  }\n\n  private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray,\n      @Flags int flags, byte[] extendedTypeScratch) throws ParserException {\n    int moofContainerChildrenSize = moof.containerChildren.size();\n    for (int i = 0; i < moofContainerChildrenSize; i++) {\n      ContainerAtom child = moof.containerChildren.get(i);\n      // TODO: Support multiple traf boxes per track in a single moof.\n      if (child.type == Atom.TYPE_traf) {\n        parseTraf(child, trackBundleArray, flags, extendedTypeScratch);\n      }\n    }\n  }\n\n  /**\n   * Parses a traf atom (defined in 14496-12).\n   */\n  private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,\n      @Flags int flags, byte[] extendedTypeScratch) throws ParserException {\n    LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);\n    TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);\n    if (trackBundle == null) {\n      return;\n    }\n\n    TrackFragment fragment = trackBundle.fragment;\n    long decodeTime = fragment.nextFragmentDecodeTime;\n    trackBundle.reset();\n\n    LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);\n    if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {\n      decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);\n    }\n\n    parseTruns(traf, trackBundle, decodeTime, flags);\n\n    TrackEncryptionBox encryptionBox = trackBundle.track\n        .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);\n\n    LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);\n    if (saiz != null) {\n      parseSaiz(encryptionBox, saiz.data, fragment);\n    }\n\n    LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);\n    if (saio != null) {\n      parseSaio(saio.data, fragment);\n    }\n\n    LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);\n    if (senc != null) {\n      parseSenc(senc.data, fragment);\n    }\n\n    LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);\n    LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);\n    if (sbgp != null && sgpd != null) {\n      parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null,\n          fragment);\n    }\n\n    int leafChildrenSize = traf.leafChildren.size();\n    for (int i = 0; i < leafChildrenSize; i++) {\n      LeafAtom atom = traf.leafChildren.get(i);\n      if (atom.type == Atom.TYPE_uuid) {\n        parseUuid(atom.data, fragment, extendedTypeScratch);\n      }\n    }\n  }\n\n  private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime,\n      @Flags int flags) {\n    int trunCount = 0;\n    int totalSampleCount = 0;\n    List<LeafAtom> leafChildren = traf.leafChildren;\n    int leafChildrenSize = leafChildren.size();\n    for (int i = 0; i < leafChildrenSize; i++) {\n      LeafAtom atom = leafChildren.get(i);\n      if (atom.type == Atom.TYPE_trun) {\n        ParsableByteArray trunData = atom.data;\n        trunData.setPosition(Atom.FULL_HEADER_SIZE);\n        int trunSampleCount = trunData.readUnsignedIntToInt();\n        if (trunSampleCount > 0) {\n          totalSampleCount += trunSampleCount;\n          trunCount++;\n        }\n      }\n    }\n    trackBundle.currentTrackRunIndex = 0;\n    trackBundle.currentSampleInTrackRun = 0;\n    trackBundle.currentSampleIndex = 0;\n    trackBundle.fragment.initTables(trunCount, totalSampleCount);\n\n    int trunIndex = 0;\n    int trunStartPosition = 0;\n    for (int i = 0; i < leafChildrenSize; i++) {\n      LeafAtom trun = leafChildren.get(i);\n      if (trun.type == Atom.TYPE_trun) {\n        trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data,\n            trunStartPosition);\n      }\n    }\n  }\n\n  private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,\n      TrackFragment out) throws ParserException {\n    int vectorSize = encryptionBox.perSampleIvSize;\n    saiz.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = saiz.readInt();\n    int flags = Atom.parseFullAtomFlags(fullAtom);\n    if ((flags & 0x01) == 1) {\n      saiz.skipBytes(8);\n    }\n    int defaultSampleInfoSize = saiz.readUnsignedByte();\n\n    int sampleCount = saiz.readUnsignedIntToInt();\n    if (sampleCount != out.sampleCount) {\n      throw new ParserException(\"Length mismatch: \" + sampleCount + \", \" + out.sampleCount);\n    }\n\n    int totalSize = 0;\n    if (defaultSampleInfoSize == 0) {\n      boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;\n      for (int i = 0; i < sampleCount; i++) {\n        int sampleInfoSize = saiz.readUnsignedByte();\n        totalSize += sampleInfoSize;\n        sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;\n      }\n    } else {\n      boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;\n      totalSize += defaultSampleInfoSize * sampleCount;\n      Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);\n    }\n    out.initEncryptionData(totalSize);\n  }\n\n  /**\n   * Parses a saio atom (defined in 14496-12).\n   *\n   * @param saio The saio atom to decode.\n   * @param out The {@link TrackFragment} to populate with data from the saio atom.\n   */\n  private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException {\n    saio.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = saio.readInt();\n    int flags = Atom.parseFullAtomFlags(fullAtom);\n    if ((flags & 0x01) == 1) {\n      saio.skipBytes(8);\n    }\n\n    int entryCount = saio.readUnsignedIntToInt();\n    if (entryCount != 1) {\n      // We only support one trun element currently, so always expect one entry.\n      throw new ParserException(\"Unexpected saio entry count: \" + entryCount);\n    }\n\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    out.auxiliaryDataPosition +=\n        version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong();\n  }\n\n  /**\n   * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and\n   * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer\n   * to any {@link TrackBundle}, {@code null} is returned and no changes are made.\n   *\n   * @param tfhd The tfhd atom to decode.\n   * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed.\n   * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd\n   *     does not refer to any {@link TrackBundle}.\n   */\n  private static TrackBundle parseTfhd(\n      ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) {\n    tfhd.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = tfhd.readInt();\n    int atomFlags = Atom.parseFullAtomFlags(fullAtom);\n    int trackId = tfhd.readInt();\n    TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);\n    if (trackBundle == null) {\n      return null;\n    }\n    if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) {\n      long baseDataPosition = tfhd.readUnsignedLongToLong();\n      trackBundle.fragment.dataPosition = baseDataPosition;\n      trackBundle.fragment.auxiliaryDataPosition = baseDataPosition;\n    }\n\n    DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;\n    int defaultSampleDescriptionIndex =\n        ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)\n            ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;\n    int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)\n        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;\n    int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)\n        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size;\n    int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)\n        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags;\n    trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex,\n        defaultSampleDuration, defaultSampleSize, defaultSampleFlags);\n    return trackBundle;\n  }\n\n  private static @Nullable TrackBundle getTrackBundle(\n      SparseArray<TrackBundle> trackBundles, int trackId) {\n    if (trackBundles.size() == 1) {\n      // Ignore track id if there is only one track. This is either because we have a side-loaded\n      // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see\n      // https://github.com/google/ExoPlayer/issues/4083).\n      return trackBundles.valueAt(/* index= */ 0);\n    }\n    return trackBundles.get(trackId);\n  }\n\n  /**\n   * Parses a tfdt atom (defined in 14496-12).\n   *\n   * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the\n   *     media, expressed in the media's timescale.\n   */\n  private static long parseTfdt(ParsableByteArray tfdt) {\n    tfdt.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = tfdt.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n    return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();\n  }\n\n  /**\n   * Parses a trun atom (defined in 14496-12).\n   *\n   * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into\n   *     which parsed data should be placed.\n   * @param index Index of the track run in the fragment.\n   * @param decodeTime The decode time of the first sample in the fragment run.\n   * @param flags Flags to allow any required workaround to be executed.\n   * @param trun The trun atom to decode.\n   * @return The starting position of samples for the next run.\n   */\n  private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime,\n      @Flags int flags, ParsableByteArray trun, int trackRunStart) {\n    trun.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = trun.readInt();\n    int atomFlags = Atom.parseFullAtomFlags(fullAtom);\n\n    Track track = trackBundle.track;\n    TrackFragment fragment = trackBundle.fragment;\n    DefaultSampleValues defaultSampleValues = fragment.header;\n\n    fragment.trunLength[index] = trun.readUnsignedIntToInt();\n    fragment.trunDataPosition[index] = fragment.dataPosition;\n    if ((atomFlags & 0x01 /* data_offset_present */) != 0) {\n      fragment.trunDataPosition[index] += trun.readInt();\n    }\n\n    boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0;\n    int firstSampleFlags = defaultSampleValues.flags;\n    if (firstSampleFlagsPresent) {\n      firstSampleFlags = trun.readUnsignedIntToInt();\n    }\n\n    boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0;\n    boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0;\n    boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0;\n    boolean sampleCompositionTimeOffsetsPresent =\n        (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0;\n\n    // Offset to the entire video timeline. In the presence of B-frames this is usually used to\n    // ensure that the first frame's presentation timestamp is zero.\n    long edtsOffset = 0;\n\n    // Currently we only support a single edit that moves the entire media timeline (indicated by\n    // duration == 0). Other uses of edit lists are uncommon and unsupported.\n    if (track.editListDurations != null && track.editListDurations.length == 1\n        && track.editListDurations[0] == 0) {\n      edtsOffset =\n          Util.scaleLargeTimestamp(\n              track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale);\n    }\n\n    int[] sampleSizeTable = fragment.sampleSizeTable;\n    int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable;\n    long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable;\n    boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable;\n\n    boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO\n        && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0;\n\n    int trackRunEnd = trackRunStart + fragment.trunLength[index];\n    long timescale = track.timescale;\n    long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime;\n    for (int i = trackRunStart; i < trackRunEnd; i++) {\n      // Use trun values if present, otherwise tfhd, otherwise trex.\n      int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()\n          : defaultSampleValues.duration;\n      int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;\n      int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags\n          : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;\n      if (sampleCompositionTimeOffsetsPresent) {\n        // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in\n        // version 0 trun boxes, however a significant number of streams violate the spec and use\n        // signed integers instead. It's safe to always decode sample offsets as signed integers\n        // here, because unsigned integers will still be parsed correctly (unless their top bit is\n        // set, which is never true in practice because sample offsets are always small).\n        int sampleOffset = trun.readInt();\n        sampleCompositionTimeOffsetTable[i] =\n            (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale);\n      } else {\n        sampleCompositionTimeOffsetTable[i] = 0;\n      }\n      sampleDecodingTimeTable[i] =\n          Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset;\n      sampleSizeTable[i] = sampleSize;\n      sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0\n          && (!workaroundEveryVideoFrameIsSyncFrame || i == 0);\n      cumulativeTime += sampleDuration;\n    }\n    fragment.nextFragmentDecodeTime = cumulativeTime;\n    return trackRunEnd;\n  }\n\n  private static void parseUuid(ParsableByteArray uuid, TrackFragment out,\n      byte[] extendedTypeScratch) throws ParserException {\n    uuid.setPosition(Atom.HEADER_SIZE);\n    uuid.readBytes(extendedTypeScratch, 0, 16);\n\n    // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.\n    if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {\n      return;\n    }\n\n    // Except for the extended type, this box is identical to a SENC box. See \"Portable encoding of\n    // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,\n    // Section 5.3.2.1.\"\n    parseSenc(uuid, 16, out);\n  }\n\n  private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException {\n    parseSenc(senc, 0, out);\n  }\n\n  private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out)\n      throws ParserException {\n    senc.setPosition(Atom.HEADER_SIZE + offset);\n    int fullAtom = senc.readInt();\n    int flags = Atom.parseFullAtomFlags(fullAtom);\n\n    if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {\n      // TODO: Implement this.\n      throw new ParserException(\"Overriding TrackEncryptionBox parameters is unsupported.\");\n    }\n\n    boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;\n    int sampleCount = senc.readUnsignedIntToInt();\n    if (sampleCount != out.sampleCount) {\n      throw new ParserException(\"Length mismatch: \" + sampleCount + \", \" + out.sampleCount);\n    }\n\n    Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);\n    out.initEncryptionData(senc.bytesLeft());\n    out.fillEncryptionData(senc);\n  }\n\n  private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType,\n      TrackFragment out) throws ParserException {\n    sbgp.setPosition(Atom.HEADER_SIZE);\n    int sbgpFullAtom = sbgp.readInt();\n    if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) {\n      // Only seig grouping type is supported.\n      return;\n    }\n    if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {\n      sbgp.skipBytes(4); // default_length.\n    }\n    if (sbgp.readInt() != 1) { // entry_count.\n      throw new ParserException(\"Entry count in sbgp != 1 (unsupported).\");\n    }\n\n    sgpd.setPosition(Atom.HEADER_SIZE);\n    int sgpdFullAtom = sgpd.readInt();\n    if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) {\n      // Only seig grouping type is supported.\n      return;\n    }\n    int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);\n    if (sgpdVersion == 1) {\n      if (sgpd.readUnsignedInt() == 0) {\n        throw new ParserException(\"Variable length description in sgpd found (unsupported)\");\n      }\n    } else if (sgpdVersion >= 2) {\n      sgpd.skipBytes(4); // default_sample_description_index.\n    }\n    if (sgpd.readUnsignedInt() != 1) { // entry_count.\n      throw new ParserException(\"Entry count in sgpd != 1 (unsupported).\");\n    }\n    // CencSampleEncryptionInformationGroupEntry\n    sgpd.skipBytes(1); // reserved = 0.\n    int patternByte = sgpd.readUnsignedByte();\n    int cryptByteBlock = (patternByte & 0xF0) >> 4;\n    int skipByteBlock = patternByte & 0x0F;\n    boolean isProtected = sgpd.readUnsignedByte() == 1;\n    if (!isProtected) {\n      return;\n    }\n    int perSampleIvSize = sgpd.readUnsignedByte();\n    byte[] keyId = new byte[16];\n    sgpd.readBytes(keyId, 0, keyId.length);\n    byte[] constantIv = null;\n    if (perSampleIvSize == 0) {\n      int constantIvSize = sgpd.readUnsignedByte();\n      constantIv = new byte[constantIvSize];\n      sgpd.readBytes(constantIv, 0, constantIvSize);\n    }\n    out.definesEncryptionData = true;\n    out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId,\n        cryptByteBlock, skipByteBlock, constantIv);\n  }\n\n  /**\n   * Parses a sidx atom (defined in 14496-12).\n   *\n   * @param atom The atom data.\n   * @param inputPosition The input position of the first byte after the atom.\n   * @return A pair consisting of the earliest presentation time in microseconds, and the parsed\n   *     {@link ChunkIndex}.\n   */\n  private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)\n      throws ParserException {\n    atom.setPosition(Atom.HEADER_SIZE);\n    int fullAtom = atom.readInt();\n    int version = Atom.parseFullAtomVersion(fullAtom);\n\n    atom.skipBytes(4);\n    long timescale = atom.readUnsignedInt();\n    long earliestPresentationTime;\n    long offset = inputPosition;\n    if (version == 0) {\n      earliestPresentationTime = atom.readUnsignedInt();\n      offset += atom.readUnsignedInt();\n    } else {\n      earliestPresentationTime = atom.readUnsignedLongToLong();\n      offset += atom.readUnsignedLongToLong();\n    }\n    long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,\n        C.MICROS_PER_SECOND, timescale);\n\n    atom.skipBytes(2);\n\n    int referenceCount = atom.readUnsignedShort();\n    int[] sizes = new int[referenceCount];\n    long[] offsets = new long[referenceCount];\n    long[] durationsUs = new long[referenceCount];\n    long[] timesUs = new long[referenceCount];\n\n    long time = earliestPresentationTime;\n    long timeUs = earliestPresentationTimeUs;\n    for (int i = 0; i < referenceCount; i++) {\n      int firstInt = atom.readInt();\n\n      int type = 0x80000000 & firstInt;\n      if (type != 0) {\n        throw new ParserException(\"Unhandled indirect reference\");\n      }\n      long referenceDuration = atom.readUnsignedInt();\n\n      sizes[i] = 0x7FFFFFFF & firstInt;\n      offsets[i] = offset;\n\n      // Calculate time and duration values such that any rounding errors are consistent. i.e. That\n      // timesUs[i] + durationsUs[i] == timesUs[i + 1].\n      timesUs[i] = timeUs;\n      time += referenceDuration;\n      timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);\n      durationsUs[i] = timeUs - timesUs[i];\n\n      atom.skipBytes(4);\n      offset += sizes[i];\n    }\n\n    return Pair.create(earliestPresentationTimeUs,\n        new ChunkIndex(sizes, offsets, durationsUs, timesUs));\n  }\n\n  private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {\n    TrackBundle nextTrackBundle = null;\n    long nextDataOffset = Long.MAX_VALUE;\n    int trackBundlesSize = trackBundles.size();\n    for (int i = 0; i < trackBundlesSize; i++) {\n      TrackFragment trackFragment = trackBundles.valueAt(i).fragment;\n      if (trackFragment.sampleEncryptionDataNeedsFill\n          && trackFragment.auxiliaryDataPosition < nextDataOffset) {\n        nextDataOffset = trackFragment.auxiliaryDataPosition;\n        nextTrackBundle = trackBundles.valueAt(i);\n      }\n    }\n    if (nextTrackBundle == null) {\n      parserState = STATE_READING_SAMPLE_START;\n      return;\n    }\n    int bytesToSkip = (int) (nextDataOffset - input.getPosition());\n    if (bytesToSkip < 0) {\n      throw new ParserException(\"Offset to encryption data was negative.\");\n    }\n    input.skipFully(bytesToSkip);\n    nextTrackBundle.fragment.fillEncryptionData(input);\n  }\n\n  /**\n   * Attempts to read the next sample in the current mdat atom. The read sample may be output or\n   * skipped.\n   *\n   * <p>If there are no more samples in the current mdat atom then the parser state is transitioned\n   * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.\n   *\n   * <p>It is possible for a sample to be partially read in the case that an exception is thrown. In\n   * this case the method can be called again to read the remainder of the sample.\n   *\n   * @param input The {@link ExtractorInput} from which to read data.\n   * @return Whether a sample was read. The read sample may have been output or skipped. False\n   *     indicates that there are no samples left to read in the current mdat.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {\n    if (parserState == STATE_READING_SAMPLE_START) {\n      if (currentTrackBundle == null) {\n        TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles);\n        if (currentTrackBundle == null) {\n          // We've run out of samples in the current mdat. Discard any trailing data and prepare to\n          // read the header of the next atom.\n          int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());\n          if (bytesToSkip < 0) {\n            throw new ParserException(\"Offset to end of mdat was negative.\");\n          }\n          input.skipFully(bytesToSkip);\n          enterReadingAtomHeaderState();\n          return false;\n        }\n\n        long nextDataPosition = currentTrackBundle.fragment\n            .trunDataPosition[currentTrackBundle.currentTrackRunIndex];\n        // We skip bytes preceding the next sample to read.\n        int bytesToSkip = (int) (nextDataPosition - input.getPosition());\n        if (bytesToSkip < 0) {\n          // Assume the sample data must be contiguous in the mdat with no preceding data.\n          Log.w(TAG, \"Ignoring negative offset to sample data.\");\n          bytesToSkip = 0;\n        }\n        input.skipFully(bytesToSkip);\n        this.currentTrackBundle = currentTrackBundle;\n      }\n\n      sampleSize = currentTrackBundle.fragment\n          .sampleSizeTable[currentTrackBundle.currentSampleIndex];\n\n      if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) {\n        input.skipFully(sampleSize);\n        currentTrackBundle.skipSampleEncryptionData();\n        if (!currentTrackBundle.next()) {\n          currentTrackBundle = null;\n        }\n        parserState = STATE_READING_SAMPLE_START;\n        return true;\n      }\n\n      if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {\n        sampleSize -= Atom.HEADER_SIZE;\n        input.skipFully(Atom.HEADER_SIZE);\n      }\n      sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData();\n      sampleSize += sampleBytesWritten;\n      parserState = STATE_READING_SAMPLE_CONTINUE;\n      sampleCurrentNalBytesRemaining = 0;\n      isAc4HeaderRequired =\n          MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType);\n    }\n\n    TrackFragment fragment = currentTrackBundle.fragment;\n    Track track = currentTrackBundle.track;\n    TrackOutput output = currentTrackBundle.output;\n    int sampleIndex = currentTrackBundle.currentSampleIndex;\n    long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;\n    if (timestampAdjuster != null) {\n      sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);\n    }\n    if (track.nalUnitLengthFieldLength != 0) {\n      // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case\n      // they're only 1 or 2 bytes long.\n      byte[] nalPrefixData = nalPrefix.data;\n      nalPrefixData[0] = 0;\n      nalPrefixData[1] = 0;\n      nalPrefixData[2] = 0;\n      int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;\n      int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;\n      // NAL units are length delimited, but the decoder requires start code delimited units.\n      // Loop until we've written the sample to the track output, replacing length delimiters with\n      // start codes as we encounter them.\n      while (sampleBytesWritten < sampleSize) {\n        if (sampleCurrentNalBytesRemaining == 0) {\n          // Read the NAL length so that we know where we find the next one, and its type.\n          input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength);\n          nalPrefix.setPosition(0);\n          int nalLengthInt = nalPrefix.readInt();\n          if (nalLengthInt < 1) {\n            throw new ParserException(\"Invalid NAL length\");\n          }\n          sampleCurrentNalBytesRemaining = nalLengthInt - 1;\n          // Write a start code for the current NAL unit.\n          nalStartCode.setPosition(0);\n          output.sampleData(nalStartCode, 4);\n          // Write the NAL unit type byte.\n          output.sampleData(nalPrefix, 1);\n          processSeiNalUnitPayload = cea608TrackOutputs.length > 0\n              && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);\n          sampleBytesWritten += 5;\n          sampleSize += nalUnitLengthFieldLengthDiff;\n        } else {\n          int writtenBytes;\n          if (processSeiNalUnitPayload) {\n            // Read and write the payload of the SEI NAL unit.\n            nalBuffer.reset(sampleCurrentNalBytesRemaining);\n            input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining);\n            output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);\n            writtenBytes = sampleCurrentNalBytesRemaining;\n            // Unescape and process the SEI NAL unit.\n            int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit());\n            // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.\n            nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);\n            nalBuffer.setLimit(unescapedLength);\n            CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs);\n          } else {\n            // Write the payload of the NAL unit.\n            writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);\n          }\n          sampleBytesWritten += writtenBytes;\n          sampleCurrentNalBytesRemaining -= writtenBytes;\n        }\n      }\n    } else {\n      if (isAc4HeaderRequired) {\n        Ac4Util.getAc4SampleHeader(sampleSize, scratch);\n        int length = scratch.limit();\n        output.sampleData(scratch, length);\n        sampleSize += length;\n        sampleBytesWritten += length;\n        isAc4HeaderRequired = false;\n      }\n      while (sampleBytesWritten < sampleSize) {\n        int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);\n        sampleBytesWritten += writtenBytes;\n      }\n    }\n\n    @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]\n        ? C.BUFFER_FLAG_KEY_FRAME : 0;\n\n    // Encryption data.\n    TrackOutput.CryptoData cryptoData = null;\n    TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted();\n    if (encryptionBox != null) {\n      sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;\n      cryptoData = encryptionBox.cryptoData;\n    }\n\n    output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);\n\n    // After we have the sampleTimeUs, we can commit all the pending metadata samples\n    outputPendingMetadataSamples(sampleTimeUs);\n    if (!currentTrackBundle.next()) {\n      currentTrackBundle = null;\n    }\n    parserState = STATE_READING_SAMPLE_START;\n    return true;\n  }\n\n  private void outputPendingMetadataSamples(long sampleTimeUs) {\n    while (!pendingMetadataSampleInfos.isEmpty()) {\n      MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();\n      pendingMetadataSampleBytes -= sampleInfo.size;\n      long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs;\n      if (timestampAdjuster != null) {\n        metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs);\n      }\n      for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {\n        emsgTrackOutput.sampleMetadata(\n            metadataTimeUs,\n            C.BUFFER_FLAG_KEY_FRAME,\n            sampleInfo.size,\n            pendingMetadataSampleBytes,\n            null);\n      }\n    }\n  }\n\n  /**\n   * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those\n   * yet to be consumed, or null if all have been consumed.\n   */\n  private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> trackBundles) {\n    TrackBundle nextTrackBundle = null;\n    long nextTrackRunOffset = Long.MAX_VALUE;\n\n    int trackBundlesSize = trackBundles.size();\n    for (int i = 0; i < trackBundlesSize; i++) {\n      TrackBundle trackBundle = trackBundles.valueAt(i);\n      if (trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount) {\n        // This track fragment contains no more runs in the next mdat box.\n      } else {\n        long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex];\n        if (trunOffset < nextTrackRunOffset) {\n          nextTrackBundle = trackBundle;\n          nextTrackRunOffset = trunOffset;\n        }\n      }\n    }\n    return nextTrackBundle;\n  }\n\n  /** Returns DrmInitData from leaf atoms. */\n  private static DrmInitData getDrmInitDataFromAtoms(List<LeafAtom> leafChildren) {\n    ArrayList<SchemeData> schemeDatas = null;\n    int leafChildrenSize = leafChildren.size();\n    for (int i = 0; i < leafChildrenSize; i++) {\n      LeafAtom child = leafChildren.get(i);\n      if (child.type == Atom.TYPE_pssh) {\n        if (schemeDatas == null) {\n          schemeDatas = new ArrayList<>();\n        }\n        byte[] psshData = child.data.data;\n        UUID uuid = PsshAtomUtil.parseUuid(psshData);\n        if (uuid == null) {\n          Log.w(TAG, \"Skipped pssh atom (failed to extract uuid)\");\n        } else {\n          schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));\n        }\n      }\n    }\n    return schemeDatas == null ? null : new DrmInitData(schemeDatas);\n  }\n\n  /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */\n  private static boolean shouldParseLeafAtom(int atom) {\n    return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd\n        || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt\n        || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex\n        || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz\n        || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid\n        || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst\n        || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;\n  }\n\n  /** Returns whether the extractor should decode a container atom with type {@code atom}. */\n  private static boolean shouldParseContainerAtom(int atom) {\n    return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia\n        || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof\n        || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;\n  }\n\n  /**\n   * Holds data corresponding to a metadata sample.\n   */\n  private static final class MetadataSampleInfo {\n\n    public final long presentationTimeDeltaUs;\n    public final int size;\n\n    public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {\n      this.presentationTimeDeltaUs = presentationTimeDeltaUs;\n      this.size = size;\n    }\n\n  }\n\n  /**\n   * Holds data corresponding to a single track.\n   */\n  private static final class TrackBundle {\n\n    public final TrackOutput output;\n    public final TrackFragment fragment;\n\n    public Track track;\n    public DefaultSampleValues defaultSampleValues;\n    public int currentSampleIndex;\n    public int currentSampleInTrackRun;\n    public int currentTrackRunIndex;\n    public int firstSampleToOutputIndex;\n\n    private final ParsableByteArray encryptionSignalByte;\n    private final ParsableByteArray defaultInitializationVector;\n\n    public TrackBundle(TrackOutput output) {\n      this.output = output;\n      fragment = new TrackFragment();\n      encryptionSignalByte = new ParsableByteArray(1);\n      defaultInitializationVector = new ParsableByteArray();\n    }\n\n    public void init(Track track, DefaultSampleValues defaultSampleValues) {\n      this.track = Assertions.checkNotNull(track);\n      this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);\n      output.format(track.format);\n      reset();\n    }\n\n    public void updateDrmInitData(DrmInitData drmInitData) {\n      TrackEncryptionBox encryptionBox =\n          track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);\n      String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;\n      output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType)));\n    }\n\n    /** Resets the current fragment and sample indices. */\n    public void reset() {\n      fragment.reset();\n      currentSampleIndex = 0;\n      currentTrackRunIndex = 0;\n      currentSampleInTrackRun = 0;\n      firstSampleToOutputIndex = 0;\n    }\n\n    /**\n     * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified\n     * seek time in the current fragment.\n     *\n     * @param timeUs The seek time, in microseconds.\n     */\n    public void seek(long timeUs) {\n      long timeMs = C.usToMs(timeUs);\n      int searchIndex = currentSampleIndex;\n      while (searchIndex < fragment.sampleCount\n          && fragment.getSamplePresentationTime(searchIndex) < timeMs) {\n        if (fragment.sampleIsSyncFrameTable[searchIndex]) {\n          firstSampleToOutputIndex = searchIndex;\n        }\n        searchIndex++;\n      }\n    }\n\n    /**\n     * Advances the indices in the bundle to point to the next sample in the current fragment. If\n     * the current sample is the last one in the current fragment, then the advanced state will be\n     * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex ==\n     * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}.\n     *\n     * @return Whether the next sample is in the same track run as the previous one.\n     */\n    public boolean next() {\n      currentSampleIndex++;\n      currentSampleInTrackRun++;\n      if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) {\n        currentTrackRunIndex++;\n        currentSampleInTrackRun = 0;\n        return false;\n      }\n      return true;\n    }\n\n    /**\n     * Outputs the encryption data for the current sample.\n     *\n     * @return The number of written bytes.\n     */\n    public int outputSampleEncryptionData() {\n      TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();\n      if (encryptionBox == null) {\n        return 0;\n      }\n\n      ParsableByteArray initializationVectorData;\n      int vectorSize;\n      if (encryptionBox.perSampleIvSize != 0) {\n        initializationVectorData = fragment.sampleEncryptionData;\n        vectorSize = encryptionBox.perSampleIvSize;\n      } else {\n        // The default initialization vector should be used.\n        byte[] initVectorData = encryptionBox.defaultInitializationVector;\n        defaultInitializationVector.reset(initVectorData, initVectorData.length);\n        initializationVectorData = defaultInitializationVector;\n        vectorSize = initVectorData.length;\n      }\n\n      boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex);\n\n      // Write the signal byte, containing the vector size and the subsample encryption flag.\n      encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));\n      encryptionSignalByte.setPosition(0);\n      output.sampleData(encryptionSignalByte, 1);\n      // Write the vector.\n      output.sampleData(initializationVectorData, vectorSize);\n      // If we don't have subsample encryption data, we're done.\n      if (!subsampleEncryption) {\n        return 1 + vectorSize;\n      }\n      // Write the subsample encryption data.\n      ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData;\n      int subsampleCount = subsampleEncryptionData.readUnsignedShort();\n      subsampleEncryptionData.skipBytes(-2);\n      int subsampleDataLength = 2 + 6 * subsampleCount;\n      output.sampleData(subsampleEncryptionData, subsampleDataLength);\n      return 1 + vectorSize + subsampleDataLength;\n    }\n\n    /** Skips the encryption data for the current sample. */\n    private void skipSampleEncryptionData() {\n      TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();\n      if (encryptionBox == null) {\n        return;\n      }\n\n      ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData;\n      if (encryptionBox.perSampleIvSize != 0) {\n        sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize);\n      }\n      if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) {\n        sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort());\n      }\n    }\n\n    private TrackEncryptionBox getEncryptionBoxIfEncrypted() {\n      int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;\n      TrackEncryptionBox encryptionBox =\n          fragment.trackEncryptionBox != null\n              ? fragment.trackEncryptionBox\n              : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);\n      return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format\n * Specification.\n */\npublic final class MdtaMetadataEntry implements Metadata.Entry {\n\n  /** The metadata key name. */\n  public final String key;\n  /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */\n  public final byte[] value;\n  /** The four byte locale indicator. */\n  public final int localeIndicator;\n  /** The four byte type indicator. */\n  public final int typeIndicator;\n\n  /** Creates a new metadata entry for the specified metadata key/value. */\n  public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {\n    this.key = key;\n    this.value = value;\n    this.localeIndicator = localeIndicator;\n    this.typeIndicator = typeIndicator;\n  }\n\n  private MdtaMetadataEntry(Parcel in) {\n    key = Util.castNonNull(in.readString());\n    value = new byte[in.readInt()];\n    in.readByteArray(value);\n    localeIndicator = in.readInt();\n    typeIndicator = in.readInt();\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    MdtaMetadataEntry other = (MdtaMetadataEntry) obj;\n    return key.equals(other.key)\n        && Arrays.equals(value, other.value)\n        && localeIndicator == other.localeIndicator\n        && typeIndicator == other.typeIndicator;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + key.hashCode();\n    result = 31 * result + Arrays.hashCode(value);\n    result = 31 * result + localeIndicator;\n    result = 31 * result + typeIndicator;\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return \"mdta: key=\" + key;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(key);\n    dest.writeInt(value.length);\n    dest.writeByteArray(value);\n    dest.writeInt(localeIndicator);\n    dest.writeInt(typeIndicator);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Creator<MdtaMetadataEntry> CREATOR =\n      new Creator<MdtaMetadataEntry>() {\n\n        @Override\n        public MdtaMetadataEntry createFromParcel(Parcel in) {\n          return new MdtaMetadataEntry(in);\n        }\n\n        @Override\n        public MdtaMetadataEntry[] newArray(int size) {\n          return new MdtaMetadataEntry[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.GaplessInfoHolder;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.ApicFrame;\nimport com.google.android.exoplayer2.metadata.id3.CommentFrame;\nimport com.google.android.exoplayer2.metadata.id3.Id3Frame;\nimport com.google.android.exoplayer2.metadata.id3.InternalFrame;\nimport com.google.android.exoplayer2.metadata.id3.TextInformationFrame;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\n\n/** Utilities for handling metadata in MP4. */\n/* package */ final class MetadataUtil {\n\n  private static final String TAG = \"MetadataUtil\";\n\n  // Codes that start with the copyright character (omitted) and have equivalent ID3 frames.\n  private static final int SHORT_TYPE_NAME_1 = 0x006e616d;\n  private static final int SHORT_TYPE_NAME_2 = 0x0074726b;\n  private static final int SHORT_TYPE_COMMENT = 0x00636d74;\n  private static final int SHORT_TYPE_YEAR = 0x00646179;\n  private static final int SHORT_TYPE_ARTIST = 0x00415254;\n  private static final int SHORT_TYPE_ENCODER = 0x00746f6f;\n  private static final int SHORT_TYPE_ALBUM = 0x00616c62;\n  private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d;\n  private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274;\n  private static final int SHORT_TYPE_LYRICS = 0x006c7972;\n  private static final int SHORT_TYPE_GENRE = 0x0067656e;\n\n  // Codes that have equivalent ID3 frames.\n  private static final int TYPE_COVER_ART = 0x636f7672;\n  private static final int TYPE_GENRE = 0x676e7265;\n  private static final int TYPE_GROUPING = 0x00677270;\n  private static final int TYPE_DISK_NUMBER = 0x6469736b;\n  private static final int TYPE_TRACK_NUMBER = 0x74726b6e;\n  private static final int TYPE_TEMPO = 0x746d706f;\n  private static final int TYPE_COMPILATION = 0x6370696c;\n  private static final int TYPE_ALBUM_ARTIST = 0x61415254;\n  private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d;\n  private static final int TYPE_SORT_ALBUM = 0x736f616c;\n  private static final int TYPE_SORT_ARTIST = 0x736f6172;\n  private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161;\n  private static final int TYPE_SORT_COMPOSER = 0x736f636f;\n\n  // Types that do not have equivalent ID3 frames.\n  private static final int TYPE_RATING = 0x72746e67;\n  private static final int TYPE_GAPLESS_ALBUM = 0x70676170;\n  private static final int TYPE_TV_SORT_SHOW = 0x736f736e;\n  private static final int TYPE_TV_SHOW = 0x74767368;\n\n  // Type for items that are intended for internal use by the player.\n  private static final int TYPE_INTERNAL = 0x2d2d2d2d;\n\n  private static final int PICTURE_TYPE_FRONT_COVER = 3;\n\n  // Standard genres.\n  private static final String[] STANDARD_GENRES = new String[] {\n      // These are the official ID3v1 genres.\n      \"Blues\", \"Classic Rock\", \"Country\", \"Dance\", \"Disco\", \"Funk\", \"Grunge\", \"Hip-Hop\", \"Jazz\",\n      \"Metal\", \"New Age\", \"Oldies\", \"Other\", \"Pop\", \"R&B\", \"Rap\", \"Reggae\", \"Rock\", \"Techno\",\n      \"Industrial\", \"Alternative\", \"Ska\", \"Death Metal\", \"Pranks\", \"Soundtrack\", \"Euro-Techno\",\n      \"Ambient\", \"Trip-Hop\", \"Vocal\", \"Jazz+Funk\", \"Fusion\", \"Trance\", \"Classical\", \"Instrumental\",\n      \"Acid\", \"House\", \"Game\", \"Sound Clip\", \"Gospel\", \"Noise\", \"AlternRock\", \"Bass\", \"Soul\",\n      \"Punk\", \"Space\", \"Meditative\", \"Instrumental Pop\", \"Instrumental Rock\", \"Ethnic\", \"Gothic\",\n      \"Darkwave\", \"Techno-Industrial\", \"Electronic\", \"Pop-Folk\", \"Eurodance\", \"Dream\",\n      \"Southern Rock\", \"Comedy\", \"Cult\", \"Gangsta\", \"Top 40\", \"Christian Rap\", \"Pop/Funk\", \"Jungle\",\n      \"Native American\", \"Cabaret\", \"New Wave\", \"Psychadelic\", \"Rave\", \"Showtunes\", \"Trailer\",\n      \"Lo-Fi\", \"Tribal\", \"Acid Punk\", \"Acid Jazz\", \"Polka\", \"Retro\", \"Musical\", \"Rock & Roll\",\n      \"Hard Rock\",\n      // These were made up by the authors of Winamp and later added to the ID3 spec.\n      \"Folk\", \"Folk-Rock\", \"National Folk\", \"Swing\", \"Fast Fusion\", \"Bebob\", \"Latin\", \"Revival\",\n      \"Celtic\", \"Bluegrass\", \"Avantgarde\", \"Gothic Rock\", \"Progressive Rock\", \"Psychedelic Rock\",\n      \"Symphonic Rock\", \"Slow Rock\", \"Big Band\", \"Chorus\", \"Easy Listening\", \"Acoustic\", \"Humour\",\n      \"Speech\", \"Chanson\", \"Opera\", \"Chamber Music\", \"Sonata\", \"Symphony\", \"Booty Bass\", \"Primus\",\n      \"Porn Groove\", \"Satire\", \"Slow Jam\", \"Club\", \"Tango\", \"Samba\", \"Folklore\", \"Ballad\",\n      \"Power Ballad\", \"Rhythmic Soul\", \"Freestyle\", \"Duet\", \"Punk Rock\", \"Drum Solo\", \"A capella\",\n      \"Euro-House\", \"Dance Hall\",\n      // These were med up by the authors of Winamp but have not been added to the ID3 spec.\n      \"Goa\", \"Drum & Bass\", \"Club-House\", \"Hardcore\", \"Terror\", \"Indie\", \"BritPop\", \"Negerpunk\",\n      \"Polsk Punk\", \"Beat\", \"Christian Gangsta Rap\", \"Heavy Metal\", \"Black Metal\", \"Crossover\",\n      \"Contemporary Christian\", \"Christian Rock\", \"Merengue\", \"Salsa\", \"Thrash Metal\", \"Anime\",\n      \"Jpop\", \"Synthpop\"\n  };\n\n  private static final String LANGUAGE_UNDEFINED = \"und\";\n\n  private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;\n  private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \\uFFFD.\n\n  private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = \"com.android.capture.fps\";\n  private static final int MDTA_TYPE_INDICATOR_FLOAT = 23;\n\n  private MetadataUtil() {}\n\n  /**\n   * Returns a {@link Format} that is the same as the input format but includes information from the\n   * specified sources of metadata.\n   */\n  public static Format getFormatWithMetadata(\n      int trackType,\n      Format format,\n      @Nullable Metadata udtaMetadata,\n      @Nullable Metadata mdtaMetadata,\n      GaplessInfoHolder gaplessInfoHolder) {\n    if (trackType == C.TRACK_TYPE_AUDIO) {\n      if (gaplessInfoHolder.hasGaplessInfo()) {\n        format =\n            format.copyWithGaplessInfo(\n                gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);\n      }\n      // We assume all udta metadata is associated with the audio track.\n      if (udtaMetadata != null) {\n        format = format.copyWithMetadata(udtaMetadata);\n      }\n    } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {\n      // Populate only metadata keys that are known to be specific to video.\n      for (int i = 0; i < mdtaMetadata.length(); i++) {\n        Metadata.Entry entry = mdtaMetadata.get(i);\n        if (entry instanceof MdtaMetadataEntry) {\n          MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;\n          if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)\n              && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) {\n            try {\n              float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get();\n              format = format.copyWithFrameRate(fps);\n              format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));\n            } catch (NumberFormatException e) {\n              Log.w(TAG, \"Ignoring invalid framerate\");\n            }\n          }\n        }\n      }\n    }\n    return format;\n  }\n\n  /**\n   * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read\n   * starting from the current position of the {@link ParsableByteArray}, and the position is\n   * advanced by the size of the element. The position is advanced even if the element's type is\n   * unrecognized.\n   *\n   * @param ilst Holds the data to be parsed.\n   * @return The parsed element, or null if the element's type was not recognized.\n   */\n  @Nullable\n  public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {\n    int position = ilst.getPosition();\n    int endPosition = position + ilst.readInt();\n    int type = ilst.readInt();\n    int typeTopByte = (type >> 24) & 0xFF;\n    try {\n      if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {\n        int shortType = type & 0x00FFFFFF;\n        if (shortType == SHORT_TYPE_COMMENT) {\n          return parseCommentAttribute(type, ilst);\n        } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {\n          return parseTextAttribute(type, \"TIT2\", ilst);\n        } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {\n          return parseTextAttribute(type, \"TCOM\", ilst);\n        } else if (shortType == SHORT_TYPE_YEAR) {\n          return parseTextAttribute(type, \"TDRC\", ilst);\n        } else if (shortType == SHORT_TYPE_ARTIST) {\n          return parseTextAttribute(type, \"TPE1\", ilst);\n        } else if (shortType == SHORT_TYPE_ENCODER) {\n          return parseTextAttribute(type, \"TSSE\", ilst);\n        } else if (shortType == SHORT_TYPE_ALBUM) {\n          return parseTextAttribute(type, \"TALB\", ilst);\n        } else if (shortType == SHORT_TYPE_LYRICS) {\n          return parseTextAttribute(type, \"USLT\", ilst);\n        } else if (shortType == SHORT_TYPE_GENRE) {\n          return parseTextAttribute(type, \"TCON\", ilst);\n        } else if (shortType == TYPE_GROUPING) {\n          return parseTextAttribute(type, \"TIT1\", ilst);\n        }\n      } else if (type == TYPE_GENRE) {\n        return parseStandardGenreAttribute(ilst);\n      } else if (type == TYPE_DISK_NUMBER) {\n        return parseIndexAndCountAttribute(type, \"TPOS\", ilst);\n      } else if (type == TYPE_TRACK_NUMBER) {\n        return parseIndexAndCountAttribute(type, \"TRCK\", ilst);\n      } else if (type == TYPE_TEMPO) {\n        return parseUint8Attribute(type, \"TBPM\", ilst, true, false);\n      } else if (type == TYPE_COMPILATION) {\n        return parseUint8Attribute(type, \"TCMP\", ilst, true, true);\n      } else if (type == TYPE_COVER_ART) {\n        return parseCoverArt(ilst);\n      } else if (type == TYPE_ALBUM_ARTIST) {\n        return parseTextAttribute(type, \"TPE2\", ilst);\n      } else if (type == TYPE_SORT_TRACK_NAME) {\n        return parseTextAttribute(type, \"TSOT\", ilst);\n      } else if (type == TYPE_SORT_ALBUM) {\n        return parseTextAttribute(type, \"TSO2\", ilst);\n      } else if (type == TYPE_SORT_ARTIST) {\n        return parseTextAttribute(type, \"TSOA\", ilst);\n      } else if (type == TYPE_SORT_ALBUM_ARTIST) {\n        return parseTextAttribute(type, \"TSOP\", ilst);\n      } else if (type == TYPE_SORT_COMPOSER) {\n        return parseTextAttribute(type, \"TSOC\", ilst);\n      } else if (type == TYPE_RATING) {\n        return parseUint8Attribute(type, \"ITUNESADVISORY\", ilst, false, false);\n      } else if (type == TYPE_GAPLESS_ALBUM) {\n        return parseUint8Attribute(type, \"ITUNESGAPLESS\", ilst, false, true);\n      } else if (type == TYPE_TV_SORT_SHOW) {\n        return parseTextAttribute(type, \"TVSHOWSORT\", ilst);\n      } else if (type == TYPE_TV_SHOW) {\n        return parseTextAttribute(type, \"TVSHOW\", ilst);\n      } else if (type == TYPE_INTERNAL) {\n        return parseInternalAttribute(ilst, endPosition);\n      }\n      Log.d(TAG, \"Skipped unknown metadata entry: \" + Atom.getAtomTypeString(type));\n      return null;\n    } finally {\n      ilst.setPosition(endPosition);\n    }\n  }\n\n  /**\n   * Parses an 'mdta' metadata entry starting at the current position in an ilst box.\n   *\n   * @param ilst The ilst box.\n   * @param endPosition The end position of the entry in the ilst box.\n   * @param key The mdta metadata entry key for the entry.\n   * @return The parsed element, or null if the entry wasn't recognized.\n   */\n  @Nullable\n  public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst(\n      ParsableByteArray ilst, int endPosition, String key) {\n    int atomPosition;\n    while ((atomPosition = ilst.getPosition()) < endPosition) {\n      int atomSize = ilst.readInt();\n      int atomType = ilst.readInt();\n      if (atomType == Atom.TYPE_data) {\n        int typeIndicator = ilst.readInt();\n        int localeIndicator = ilst.readInt();\n        int dataSize = atomSize - 16;\n        byte[] value = new byte[dataSize];\n        ilst.readBytes(value, 0, dataSize);\n        return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator);\n      }\n      ilst.setPosition(atomPosition + atomSize);\n    }\n    return null;\n  }\n\n  @Nullable\n  private static TextInformationFrame parseTextAttribute(\n      int type, String id, ParsableByteArray data) {\n    int atomSize = data.readInt();\n    int atomType = data.readInt();\n    if (atomType == Atom.TYPE_data) {\n      data.skipBytes(8); // version (1), flags (3), empty (4)\n      String value = data.readNullTerminatedString(atomSize - 16);\n      return new TextInformationFrame(id, /* description= */ null, value);\n    }\n    Log.w(TAG, \"Failed to parse text attribute: \" + Atom.getAtomTypeString(type));\n    return null;\n  }\n\n  @Nullable\n  private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {\n    int atomSize = data.readInt();\n    int atomType = data.readInt();\n    if (atomType == Atom.TYPE_data) {\n      data.skipBytes(8); // version (1), flags (3), empty (4)\n      String value = data.readNullTerminatedString(atomSize - 16);\n      return new CommentFrame(LANGUAGE_UNDEFINED, value, value);\n    }\n    Log.w(TAG, \"Failed to parse comment attribute: \" + Atom.getAtomTypeString(type));\n    return null;\n  }\n\n  @Nullable\n  private static Id3Frame parseUint8Attribute(\n      int type,\n      String id,\n      ParsableByteArray data,\n      boolean isTextInformationFrame,\n      boolean isBoolean) {\n    int value = parseUint8AttributeValue(data);\n    if (isBoolean) {\n      value = Math.min(1, value);\n    }\n    if (value >= 0) {\n      return isTextInformationFrame\n          ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value))\n          : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));\n    }\n    Log.w(TAG, \"Failed to parse uint8 attribute: \" + Atom.getAtomTypeString(type));\n    return null;\n  }\n\n  @Nullable\n  private static TextInformationFrame parseIndexAndCountAttribute(\n      int type, String attributeName, ParsableByteArray data) {\n    int atomSize = data.readInt();\n    int atomType = data.readInt();\n    if (atomType == Atom.TYPE_data && atomSize >= 22) {\n      data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)\n      int index = data.readUnsignedShort();\n      if (index > 0) {\n        String value = \"\" + index;\n        int count = data.readUnsignedShort();\n        if (count > 0) {\n          value += \"/\" + count;\n        }\n        return new TextInformationFrame(attributeName, /* description= */ null, value);\n      }\n    }\n    Log.w(TAG, \"Failed to parse index/count attribute: \" + Atom.getAtomTypeString(type));\n    return null;\n  }\n\n  @Nullable\n  private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {\n    int genreCode = parseUint8AttributeValue(data);\n    String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)\n        ? STANDARD_GENRES[genreCode - 1] : null;\n    if (genreString != null) {\n      return new TextInformationFrame(\"TCON\", /* description= */ null, genreString);\n    }\n    Log.w(TAG, \"Failed to parse standard genre code\");\n    return null;\n  }\n\n  @Nullable\n  private static ApicFrame parseCoverArt(ParsableByteArray data) {\n    int atomSize = data.readInt();\n    int atomType = data.readInt();\n    if (atomType == Atom.TYPE_data) {\n      int fullVersionInt = data.readInt();\n      int flags = Atom.parseFullAtomFlags(fullVersionInt);\n      String mimeType = flags == 13 ? \"image/jpeg\" : flags == 14 ? \"image/png\" : null;\n      if (mimeType == null) {\n        Log.w(TAG, \"Unrecognized cover art flags: \" + flags);\n        return null;\n      }\n      data.skipBytes(4); // empty (4)\n      byte[] pictureData = new byte[atomSize - 16];\n      data.readBytes(pictureData, 0, pictureData.length);\n      return new ApicFrame(\n          mimeType,\n          /* description= */ null,\n          /* pictureType= */ PICTURE_TYPE_FRONT_COVER,\n          pictureData);\n    }\n    Log.w(TAG, \"Failed to parse cover art attribute\");\n    return null;\n  }\n\n  @Nullable\n  private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {\n    String domain = null;\n    String name = null;\n    int dataAtomPosition = -1;\n    int dataAtomSize = -1;\n    while (data.getPosition() < endPosition) {\n      int atomPosition = data.getPosition();\n      int atomSize = data.readInt();\n      int atomType = data.readInt();\n      data.skipBytes(4); // version (1), flags (3)\n      if (atomType == Atom.TYPE_mean) {\n        domain = data.readNullTerminatedString(atomSize - 12);\n      } else if (atomType == Atom.TYPE_name) {\n        name = data.readNullTerminatedString(atomSize - 12);\n      } else {\n        if (atomType == Atom.TYPE_data) {\n          dataAtomPosition = atomPosition;\n          dataAtomSize = atomSize;\n        }\n        data.skipBytes(atomSize - 12);\n      }\n    }\n    if (domain == null || name == null || dataAtomPosition == -1) {\n      return null;\n    }\n    data.setPosition(dataAtomPosition);\n    data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)\n    String value = data.readNullTerminatedString(dataAtomSize - 16);\n    return new InternalFrame(domain, name, value);\n  }\n\n  private static int parseUint8AttributeValue(ParsableByteArray data) {\n    data.skipBytes(4); // atomSize\n    int atomType = data.readInt();\n    if (atomType == Atom.TYPE_data) {\n      data.skipBytes(8); // version (1), flags (3), empty (4)\n      return data.readUnsignedByte();\n    }\n    Log.w(TAG, \"Failed to parse uint8 attribute value\");\n    return -1;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.Ac4Util;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.GaplessInfoHolder;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Extracts data from the MP4 container format.\n */\npublic final class Mp4Extractor implements Extractor, SeekMap {\n\n  /** Factory for {@link Mp4Extractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag value is {@link\n   * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS})\n  public @interface Flags {}\n  /**\n   * Flag to ignore any edit lists in the stream.\n   */\n  public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1;\n\n  /** Parser states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE})\n  private @interface State {}\n\n  private static final int STATE_READING_ATOM_HEADER = 0;\n  private static final int STATE_READING_ATOM_PAYLOAD = 1;\n  private static final int STATE_READING_SAMPLE = 2;\n\n  /** Brand stored in the ftyp atom for QuickTime media. */\n  private static final int BRAND_QUICKTIME = 0x71742020;\n\n  /**\n   * When seeking within the source, if the offset is greater than or equal to this value (or the\n   * offset is negative), the source will be reloaded.\n   */\n  private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;\n\n  /**\n   * For poorly interleaved streams, the maximum byte difference one track is allowed to be read\n   * ahead before the source will be reloaded at a new position to read another track.\n   */\n  private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024;\n\n  private final @Flags int flags;\n\n  // Temporary arrays.\n  private final ParsableByteArray nalStartCode;\n  private final ParsableByteArray nalLength;\n  private final ParsableByteArray scratch;\n\n  private final ParsableByteArray atomHeader;\n  private final ArrayDeque<ContainerAtom> containerAtoms;\n\n  @State private int parserState;\n  private int atomType;\n  private long atomSize;\n  private int atomHeaderBytesRead;\n  private ParsableByteArray atomData;\n\n  private int sampleTrackIndex;\n  private int sampleBytesWritten;\n  private int sampleCurrentNalBytesRemaining;\n  private boolean isAc4HeaderRequired;\n\n  // Extractor outputs.\n  private ExtractorOutput extractorOutput;\n  private Mp4Track[] tracks;\n  private long[][] accumulatedSampleSizes;\n  private int firstVideoTrackIndex;\n  private long durationUs;\n  private boolean isQuickTime;\n\n  /**\n   * Creates a new extractor for unfragmented MP4 streams.\n   */\n  public Mp4Extractor() {\n    this(0);\n  }\n\n  /**\n   * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the\n   * extractor's behavior.\n   *\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public Mp4Extractor(@Flags int flags) {\n    this.flags = flags;\n    atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);\n    containerAtoms = new ArrayDeque<>();\n    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);\n    nalLength = new ParsableByteArray(4);\n    scratch = new ParsableByteArray();\n    sampleTrackIndex = C.INDEX_UNSET;\n  }\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return Sniffer.sniffUnfragmented(input);\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    extractorOutput = output;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    containerAtoms.clear();\n    atomHeaderBytesRead = 0;\n    sampleTrackIndex = C.INDEX_UNSET;\n    sampleBytesWritten = 0;\n    sampleCurrentNalBytesRemaining = 0;\n    isAc4HeaderRequired = false;\n    if (position == 0) {\n      enterReadingAtomHeaderState();\n    } else if (tracks != null) {\n      updateSampleIndices(timeUs);\n    }\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    while (true) {\n      switch (parserState) {\n        case STATE_READING_ATOM_HEADER:\n          if (!readAtomHeader(input)) {\n            return RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_READING_ATOM_PAYLOAD:\n          if (readAtomPayload(input, seekPosition)) {\n            return RESULT_SEEK;\n          }\n          break;\n        case STATE_READING_SAMPLE:\n          return readSample(input, seekPosition);\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  // SeekMap implementation.\n\n  @Override\n  public boolean isSeekable() {\n    return true;\n  }\n\n  @Override\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    if (tracks.length == 0) {\n      return new SeekPoints(SeekPoint.START);\n    }\n\n    long firstTimeUs;\n    long firstOffset;\n    long secondTimeUs = C.TIME_UNSET;\n    long secondOffset = C.POSITION_UNSET;\n\n    // If we have a video track, use it to establish one or two seek points.\n    if (firstVideoTrackIndex != C.INDEX_UNSET) {\n      TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable;\n      int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs);\n      if (sampleIndex == C.INDEX_UNSET) {\n        return new SeekPoints(SeekPoint.START);\n      }\n      long sampleTimeUs = sampleTable.timestampsUs[sampleIndex];\n      firstTimeUs = sampleTimeUs;\n      firstOffset = sampleTable.offsets[sampleIndex];\n      if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) {\n        int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);\n        if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) {\n          secondTimeUs = sampleTable.timestampsUs[secondSampleIndex];\n          secondOffset = sampleTable.offsets[secondSampleIndex];\n        }\n      }\n    } else {\n      firstTimeUs = timeUs;\n      firstOffset = Long.MAX_VALUE;\n    }\n\n    // Take into account other tracks.\n    for (int i = 0; i < tracks.length; i++) {\n      if (i != firstVideoTrackIndex) {\n        TrackSampleTable sampleTable = tracks[i].sampleTable;\n        firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);\n        if (secondTimeUs != C.TIME_UNSET) {\n          secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);\n        }\n      }\n    }\n\n    SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset);\n    if (secondTimeUs == C.TIME_UNSET) {\n      return new SeekPoints(firstSeekPoint);\n    } else {\n      SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset);\n      return new SeekPoints(firstSeekPoint, secondSeekPoint);\n    }\n  }\n\n  // Private methods.\n\n  private void enterReadingAtomHeaderState() {\n    parserState = STATE_READING_ATOM_HEADER;\n    atomHeaderBytesRead = 0;\n  }\n\n  private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {\n    if (atomHeaderBytesRead == 0) {\n      // Read the standard length atom header.\n      if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {\n        return false;\n      }\n      atomHeaderBytesRead = Atom.HEADER_SIZE;\n      atomHeader.setPosition(0);\n      atomSize = atomHeader.readUnsignedInt();\n      atomType = atomHeader.readInt();\n    }\n\n    if (atomSize == Atom.DEFINES_LARGE_SIZE) {\n      // Read the large size.\n      int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;\n      input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);\n      atomHeaderBytesRead += headerBytesRemaining;\n      atomSize = atomHeader.readUnsignedLongToLong();\n    } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {\n      // The atom extends to the end of the file. Note that if the atom is within a container we can\n      // work out its size even if the input length is unknown.\n      long endPosition = input.getLength();\n      if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) {\n        endPosition = containerAtoms.peek().endPosition;\n      }\n      if (endPosition != C.LENGTH_UNSET) {\n        atomSize = endPosition - input.getPosition() + atomHeaderBytesRead;\n      }\n    }\n\n    if (atomSize < atomHeaderBytesRead) {\n      throw new ParserException(\"Atom size less than header length (unsupported).\");\n    }\n\n    if (shouldParseContainerAtom(atomType)) {\n      long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;\n      if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) {\n        maybeSkipRemainingMetaAtomHeaderBytes(input);\n      }\n      containerAtoms.push(new ContainerAtom(atomType, endPosition));\n      if (atomSize == atomHeaderBytesRead) {\n        processAtomEnded(endPosition);\n      } else {\n        // Start reading the first child atom.\n        enterReadingAtomHeaderState();\n      }\n    } else if (shouldParseLeafAtom(atomType)) {\n      // We don't support parsing of leaf atoms that define extended atom sizes, or that have\n      // lengths greater than Integer.MAX_VALUE.\n      Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);\n      Assertions.checkState(atomSize <= Integer.MAX_VALUE);\n      atomData = new ParsableByteArray((int) atomSize);\n      System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);\n      parserState = STATE_READING_ATOM_PAYLOAD;\n    } else {\n      atomData = null;\n      parserState = STATE_READING_ATOM_PAYLOAD;\n    }\n\n    return true;\n  }\n\n  /**\n   * Processes the atom payload. If {@link #atomData} is null and the size is at or above the\n   * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should\n   * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.\n   */\n  private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)\n      throws IOException, InterruptedException {\n    long atomPayloadSize = atomSize - atomHeaderBytesRead;\n    long atomEndPosition = input.getPosition() + atomPayloadSize;\n    boolean seekRequired = false;\n    if (atomData != null) {\n      input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize);\n      if (atomType == Atom.TYPE_ftyp) {\n        isQuickTime = processFtypAtom(atomData);\n      } else if (!containerAtoms.isEmpty()) {\n        containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));\n      }\n    } else {\n      // We don't need the data. Skip or seek, depending on how large the atom is.\n      if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) {\n        input.skipFully((int) atomPayloadSize);\n      } else {\n        positionHolder.position = input.getPosition() + atomPayloadSize;\n        seekRequired = true;\n      }\n    }\n    processAtomEnded(atomEndPosition);\n    return seekRequired && parserState != STATE_READING_SAMPLE;\n  }\n\n  private void processAtomEnded(long atomEndPosition) throws ParserException {\n    while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {\n      ContainerAtom containerAtom = containerAtoms.pop();\n      if (containerAtom.type == Atom.TYPE_moov) {\n        // We've reached the end of the moov atom. Process it and prepare to read samples.\n        processMoovAtom(containerAtom);\n        containerAtoms.clear();\n        parserState = STATE_READING_SAMPLE;\n      } else if (!containerAtoms.isEmpty()) {\n        containerAtoms.peek().add(containerAtom);\n      }\n    }\n    if (parserState != STATE_READING_SAMPLE) {\n      enterReadingAtomHeaderState();\n    }\n  }\n\n  /**\n   * Updates the stored track metadata to reflect the contents of the specified moov atom.\n   */\n  private void processMoovAtom(ContainerAtom moov) throws ParserException {\n    int firstVideoTrackIndex = C.INDEX_UNSET;\n    long durationUs = C.TIME_UNSET;\n    List<Mp4Track> tracks = new ArrayList<>();\n\n    // Process metadata.\n    Metadata udtaMetadata = null;\n    GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();\n    Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);\n    if (udta != null) {\n      udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);\n      if (udtaMetadata != null) {\n        gaplessInfoHolder.setFromMetadata(udtaMetadata);\n      }\n    }\n    Metadata mdtaMetadata = null;\n    ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta);\n    if (meta != null) {\n      mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);\n    }\n\n    boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;\n    ArrayList<TrackSampleTable> trackSampleTables =\n        getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);\n\n    int trackCount = trackSampleTables.size();\n    for (int i = 0; i < trackCount; i++) {\n      TrackSampleTable trackSampleTable = trackSampleTables.get(i);\n      Track track = trackSampleTable.track;\n      long trackDurationUs =\n          track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs;\n      durationUs = Math.max(durationUs, trackDurationUs);\n      Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,\n          extractorOutput.track(i, track.type));\n\n      // Each sample has up to three bytes of overhead for the start code that replaces its length.\n      // Allow ten source samples per output sample, like the platform extractor.\n      int maxInputSize = trackSampleTable.maximumSize + 3 * 10;\n      Format format = track.format.copyWithMaxInputSize(maxInputSize);\n      if (track.type == C.TRACK_TYPE_VIDEO\n          && trackDurationUs > 0\n          && trackSampleTable.sampleCount > 1) {\n        float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f);\n        format = format.copyWithFrameRate(frameRate);\n      }\n      format =\n          MetadataUtil.getFormatWithMetadata(\n              track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);\n      mp4Track.trackOutput.format(format);\n\n      if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {\n        firstVideoTrackIndex = tracks.size();\n      }\n      tracks.add(mp4Track);\n    }\n    this.firstVideoTrackIndex = firstVideoTrackIndex;\n    this.durationUs = durationUs;\n    this.tracks = tracks.toArray(new Mp4Track[0]);\n    accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks);\n\n    extractorOutput.endTracks();\n    extractorOutput.seekMap(this);\n  }\n\n  private ArrayList<TrackSampleTable> getTrackSampleTables(\n      ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)\n      throws ParserException {\n    ArrayList<TrackSampleTable> trackSampleTables = new ArrayList<>();\n    for (int i = 0; i < moov.containerChildren.size(); i++) {\n      ContainerAtom atom = moov.containerChildren.get(i);\n      if (atom.type != Atom.TYPE_trak) {\n        continue;\n      }\n      Track track =\n          AtomParsers.parseTrak(\n              atom,\n              moov.getLeafAtomOfType(Atom.TYPE_mvhd),\n              /* duration= */ C.TIME_UNSET,\n              /* drmInitData= */ null,\n              ignoreEditLists,\n              isQuickTime);\n      if (track == null) {\n        continue;\n      }\n      ContainerAtom stblAtom =\n          atom.getContainerAtomOfType(Atom.TYPE_mdia)\n              .getContainerAtomOfType(Atom.TYPE_minf)\n              .getContainerAtomOfType(Atom.TYPE_stbl);\n      TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);\n      if (trackSampleTable.sampleCount == 0) {\n        continue;\n      }\n      trackSampleTables.add(trackSampleTable);\n    }\n    return trackSampleTables;\n  }\n\n  /**\n   * Attempts to extract the next sample in the current mdat atom for the specified track.\n   * <p>\n   * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in\n   * {@code positionHolder}.\n   * <p>\n   * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns\n   * {@link #RESULT_CONTINUE}.\n   *\n   * @param input The {@link ExtractorInput} from which to read data.\n   * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the\n   *     position of the required data.\n   * @return One of the {@code RESULT_*} flags in {@link Extractor}.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  private int readSample(ExtractorInput input, PositionHolder positionHolder)\n      throws IOException, InterruptedException {\n    long inputPosition = input.getPosition();\n    if (sampleTrackIndex == C.INDEX_UNSET) {\n      sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition);\n      if (sampleTrackIndex == C.INDEX_UNSET) {\n        return RESULT_END_OF_INPUT;\n      }\n      isAc4HeaderRequired =\n          MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType);\n    }\n    Mp4Track track = tracks[sampleTrackIndex];\n    TrackOutput trackOutput = track.trackOutput;\n    int sampleIndex = track.sampleIndex;\n    long position = track.sampleTable.offsets[sampleIndex];\n    int sampleSize = track.sampleTable.sizes[sampleIndex];\n    long skipAmount = position - inputPosition + sampleBytesWritten;\n    if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {\n      positionHolder.position = position;\n      return RESULT_SEEK;\n    }\n    if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {\n      // The sample information is contained in a cdat atom. The header must be discarded for\n      // committing.\n      skipAmount += Atom.HEADER_SIZE;\n      sampleSize -= Atom.HEADER_SIZE;\n    }\n    input.skipFully((int) skipAmount);\n    if (track.track.nalUnitLengthFieldLength != 0) {\n      // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case\n      // they're only 1 or 2 bytes long.\n      byte[] nalLengthData = nalLength.data;\n      nalLengthData[0] = 0;\n      nalLengthData[1] = 0;\n      nalLengthData[2] = 0;\n      int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength;\n      int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength;\n      // NAL units are length delimited, but the decoder requires start code delimited units.\n      // Loop until we've written the sample to the track output, replacing length delimiters with\n      // start codes as we encounter them.\n      while (sampleBytesWritten < sampleSize) {\n        if (sampleCurrentNalBytesRemaining == 0) {\n          // Read the NAL length so that we know where we find the next one.\n          input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);\n          nalLength.setPosition(0);\n          int nalLengthInt = nalLength.readInt();\n          if (nalLengthInt < 0) {\n            throw new ParserException(\"Invalid NAL length\");\n          }\n          sampleCurrentNalBytesRemaining = nalLengthInt;\n          // Write a start code for the current NAL unit.\n          nalStartCode.setPosition(0);\n          trackOutput.sampleData(nalStartCode, 4);\n          sampleBytesWritten += 4;\n          sampleSize += nalUnitLengthFieldLengthDiff;\n        } else {\n          // Write the payload of the NAL unit.\n          int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);\n          sampleBytesWritten += writtenBytes;\n          sampleCurrentNalBytesRemaining -= writtenBytes;\n        }\n      }\n    } else {\n      if (isAc4HeaderRequired) {\n        Ac4Util.getAc4SampleHeader(sampleSize, scratch);\n        int length = scratch.limit();\n        trackOutput.sampleData(scratch, length);\n        sampleSize += length;\n        sampleBytesWritten += length;\n        isAc4HeaderRequired = false;\n      }\n      while (sampleBytesWritten < sampleSize) {\n        int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);\n        sampleBytesWritten += writtenBytes;\n        sampleCurrentNalBytesRemaining -= writtenBytes;\n      }\n    }\n    trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],\n        track.sampleTable.flags[sampleIndex], sampleSize, 0, null);\n    track.sampleIndex++;\n    sampleTrackIndex = C.INDEX_UNSET;\n    sampleBytesWritten = 0;\n    sampleCurrentNalBytesRemaining = 0;\n    return RESULT_CONTINUE;\n  }\n\n  /**\n   * Returns the index of the track that contains the next sample to be read, or {@link\n   * C#INDEX_UNSET} if no samples remain.\n   *\n   * <p>The preferred choice is the sample with the smallest offset not requiring a source reload,\n   * or if not available the sample with the smallest overall offset to avoid subsequent source\n   * reloads.\n   *\n   * <p>To deal with poor sample interleaving, we also check whether the required memory to catch up\n   * with the next logical sample (based on sample time) exceeds {@link\n   * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even\n   * though it may require a source reload.\n   */\n  private int getTrackIndexOfNextReadSample(long inputPosition) {\n    long preferredSkipAmount = Long.MAX_VALUE;\n    boolean preferredRequiresReload = true;\n    int preferredTrackIndex = C.INDEX_UNSET;\n    long preferredAccumulatedBytes = Long.MAX_VALUE;\n    long minAccumulatedBytes = Long.MAX_VALUE;\n    boolean minAccumulatedBytesRequiresReload = true;\n    int minAccumulatedBytesTrackIndex = C.INDEX_UNSET;\n    for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {\n      Mp4Track track = tracks[trackIndex];\n      int sampleIndex = track.sampleIndex;\n      if (sampleIndex == track.sampleTable.sampleCount) {\n        continue;\n      }\n      long sampleOffset = track.sampleTable.offsets[sampleIndex];\n      long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex];\n      long skipAmount = sampleOffset - inputPosition;\n      boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE;\n      if ((!requiresReload && preferredRequiresReload)\n          || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) {\n        preferredRequiresReload = requiresReload;\n        preferredSkipAmount = skipAmount;\n        preferredTrackIndex = trackIndex;\n        preferredAccumulatedBytes = sampleAccumulatedBytes;\n      }\n      if (sampleAccumulatedBytes < minAccumulatedBytes) {\n        minAccumulatedBytes = sampleAccumulatedBytes;\n        minAccumulatedBytesRequiresReload = requiresReload;\n        minAccumulatedBytesTrackIndex = trackIndex;\n      }\n    }\n    return minAccumulatedBytes == Long.MAX_VALUE\n            || !minAccumulatedBytesRequiresReload\n            || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM\n        ? preferredTrackIndex\n        : minAccumulatedBytesTrackIndex;\n  }\n\n  /**\n   * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}.\n   */\n  private void updateSampleIndices(long timeUs) {\n    for (Mp4Track track : tracks) {\n      TrackSampleTable sampleTable = track.sampleTable;\n      int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);\n      if (sampleIndex == C.INDEX_UNSET) {\n        // Handle the case where the requested time is before the first synchronization sample.\n        sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);\n      }\n      track.sampleIndex = sampleIndex;\n    }\n  }\n\n  /**\n   * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code\n   * input}.\n   *\n   * <p>Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional\n   * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005).\n   * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly,\n   * we can't rely on the file type though. Instead we must check the 8 bytes after the common\n   * header bytes ourselves.\n   */\n  private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input)\n      throws IOException, InterruptedException {\n    scratch.reset(8);\n    // Peek the next 8 bytes which can be either\n    // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]\n    // (qt)  [4 byte size of next atom      ][4 byte hdlr atom type   ]\n    // In case of (iso) we need to skip the next 4 bytes.\n    input.peekFully(scratch.data, 0, 8);\n    scratch.skipBytes(4);\n    if (scratch.readInt() == Atom.TYPE_hdlr) {\n      input.resetPeekPosition();\n    } else {\n      input.skipFully(4);\n    }\n  }\n\n  /**\n   * For each sample of each track, calculates accumulated size of all samples which need to be read\n   * before this sample can be used.\n   */\n  private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) {\n    long[][] accumulatedSampleSizes = new long[tracks.length][];\n    int[] nextSampleIndex = new int[tracks.length];\n    long[] nextSampleTimesUs = new long[tracks.length];\n    boolean[] tracksFinished = new boolean[tracks.length];\n    for (int i = 0; i < tracks.length; i++) {\n      accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount];\n      nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0];\n    }\n    long accumulatedSampleSize = 0;\n    int finishedTracks = 0;\n    while (finishedTracks < tracks.length) {\n      long minTimeUs = Long.MAX_VALUE;\n      int minTimeTrackIndex = -1;\n      for (int i = 0; i < tracks.length; i++) {\n        if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) {\n          minTimeTrackIndex = i;\n          minTimeUs = nextSampleTimesUs[i];\n        }\n      }\n      int trackSampleIndex = nextSampleIndex[minTimeTrackIndex];\n      accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize;\n      accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex];\n      nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex;\n      if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) {\n        nextSampleTimesUs[minTimeTrackIndex] =\n            tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex];\n      } else {\n        tracksFinished[minTimeTrackIndex] = true;\n        finishedTracks++;\n      }\n    }\n    return accumulatedSampleSizes;\n  }\n\n  /**\n   * Adjusts a seek point offset to take into account the track with the given {@code sampleTable},\n   * for a given {@code seekTimeUs}.\n   *\n   * @param sampleTable The sample table to use.\n   * @param seekTimeUs The seek time in microseconds.\n   * @param offset The current offset.\n   * @return The adjusted offset.\n   */\n  private static long maybeAdjustSeekOffset(\n      TrackSampleTable sampleTable, long seekTimeUs, long offset) {\n    int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs);\n    if (sampleIndex == C.INDEX_UNSET) {\n      return offset;\n    }\n    long sampleOffset = sampleTable.offsets[sampleIndex];\n    return Math.min(sampleOffset, offset);\n  }\n\n  /**\n   * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of\n   * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if\n   * there are no synchronization samples in the table.\n   *\n   * @param sampleTable The sample table in which to locate a synchronization sample.\n   * @param timeUs A time in microseconds.\n   * @return The index of the synchronization sample before or at {@code timeUs}, or the index of\n   *     the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET}\n   *     if there are no synchronization samples in the table.\n   */\n  private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) {\n    int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);\n    if (sampleIndex == C.INDEX_UNSET) {\n      // Handle the case where the requested time is before the first synchronization sample.\n      sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);\n    }\n    return sampleIndex;\n  }\n\n  /**\n   * Process an ftyp atom to determine whether the media is QuickTime.\n   *\n   * @param atomData The ftyp atom data.\n   * @return Whether the media is QuickTime.\n   */\n  private static boolean processFtypAtom(ParsableByteArray atomData) {\n    atomData.setPosition(Atom.HEADER_SIZE);\n    int majorBrand = atomData.readInt();\n    if (majorBrand == BRAND_QUICKTIME) {\n      return true;\n    }\n    atomData.skipBytes(4); // minor_version\n    while (atomData.bytesLeft() > 0) {\n      if (atomData.readInt() == BRAND_QUICKTIME) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */\n  private static boolean shouldParseLeafAtom(int atom) {\n    return atom == Atom.TYPE_mdhd\n        || atom == Atom.TYPE_mvhd\n        || atom == Atom.TYPE_hdlr\n        || atom == Atom.TYPE_stsd\n        || atom == Atom.TYPE_stts\n        || atom == Atom.TYPE_stss\n        || atom == Atom.TYPE_ctts\n        || atom == Atom.TYPE_elst\n        || atom == Atom.TYPE_stsc\n        || atom == Atom.TYPE_stsz\n        || atom == Atom.TYPE_stz2\n        || atom == Atom.TYPE_stco\n        || atom == Atom.TYPE_co64\n        || atom == Atom.TYPE_tkhd\n        || atom == Atom.TYPE_ftyp\n        || atom == Atom.TYPE_udta\n        || atom == Atom.TYPE_keys\n        || atom == Atom.TYPE_ilst;\n  }\n\n  /** Returns whether the extractor should decode a container atom with type {@code atom}. */\n  private static boolean shouldParseContainerAtom(int atom) {\n    return atom == Atom.TYPE_moov\n        || atom == Atom.TYPE_trak\n        || atom == Atom.TYPE_mdia\n        || atom == Atom.TYPE_minf\n        || atom == Atom.TYPE_stbl\n        || atom == Atom.TYPE_edts\n        || atom == Atom.TYPE_meta;\n  }\n\n  private static final class Mp4Track {\n\n    public final Track track;\n    public final TrackSampleTable sampleTable;\n    public final TrackOutput trackOutput;\n\n    public int sampleIndex;\n\n    public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {\n      this.track = track;\n      this.sampleTable = sampleTable;\n      this.trackOutput = trackOutput;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\nimport java.util.UUID;\n\n/**\n * Utility methods for handling PSSH atoms.\n */\npublic final class PsshAtomUtil {\n\n  private static final String TAG = \"PsshAtomUtil\";\n\n  private PsshAtomUtil() {}\n\n  /**\n   * Builds a version 0 PSSH atom for a given system id, containing the given data.\n   *\n   * @param systemId The system id of the scheme.\n   * @param data The scheme specific data.\n   * @return The PSSH atom.\n   */\n  public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) {\n    return buildPsshAtom(systemId, null, data);\n  }\n\n  /**\n   * Builds a PSSH atom for the given system id, containing the given key ids and data.\n   *\n   * @param systemId The system id of the scheme.\n   * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom.\n   * @param data The scheme specific data.\n   * @return The PSSH atom.\n   */\n  // dereference of possibly-null reference keyId\n  @SuppressWarnings({\"ParameterNotNullable\", \"nullness:dereference.of.nullable\"})\n  public static byte[] buildPsshAtom(\n      UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {\n    int dataLength = data != null ? data.length : 0;\n    int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength;\n    if (keyIds != null) {\n      psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */;\n    }\n    ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);\n    psshBox.putInt(psshBoxLength);\n    psshBox.putInt(Atom.TYPE_pssh);\n    psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */);\n    psshBox.putLong(systemId.getMostSignificantBits());\n    psshBox.putLong(systemId.getLeastSignificantBits());\n    if (keyIds != null) {\n      psshBox.putInt(keyIds.length);\n      for (UUID keyId : keyIds) {\n        psshBox.putLong(keyId.getMostSignificantBits());\n        psshBox.putLong(keyId.getLeastSignificantBits());\n      }\n    }\n    if (data != null && data.length != 0) {\n      psshBox.putInt(data.length);\n      psshBox.put(data);\n    } // Else the last 4 bytes are a 0 DataSize.\n    return psshBox.array();\n  }\n\n  /**\n   * Returns whether the data is a valid PSSH atom.\n   *\n   * @param data The data to parse.\n   * @return Whether the data is a valid PSSH atom.\n   */\n  public static boolean isPsshAtom(byte[] data) {\n    return parsePsshAtom(data) != null;\n  }\n\n  /**\n   * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported.\n   *\n   * <p>The UUID is only parsed if the data is a valid PSSH atom.\n   *\n   * @param atom The atom to parse.\n   * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an\n   *     unsupported version.\n   */\n  public static @Nullable UUID parseUuid(byte[] atom) {\n    PsshAtom parsedAtom = parsePsshAtom(atom);\n    if (parsedAtom == null) {\n      return null;\n    }\n    return parsedAtom.uuid;\n  }\n\n  /**\n   * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported.\n   * <p>\n   * The version is only parsed if the data is a valid PSSH atom.\n   *\n   * @param atom The atom to parse.\n   * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has\n   *     an unsupported version.\n   */\n  public static int parseVersion(byte[] atom) {\n    PsshAtom parsedAtom = parsePsshAtom(atom);\n    if (parsedAtom == null) {\n      return -1;\n    }\n    return parsedAtom.version;\n  }\n\n  /**\n   * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported.\n   *\n   * <p>The scheme specific data is only parsed if the data is a valid PSSH atom matching the given\n   * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null.\n   *\n   * @param atom The atom to parse.\n   * @param uuid The required UUID of the PSSH atom, or null to accept any UUID.\n   * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the\n   *     PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID.\n   */\n  public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {\n    PsshAtom parsedAtom = parsePsshAtom(atom);\n    if (parsedAtom == null) {\n      return null;\n    }\n    if (uuid != null && !uuid.equals(parsedAtom.uuid)) {\n      Log.w(TAG, \"UUID mismatch. Expected: \" + uuid + \", got: \" + parsedAtom.uuid + \".\");\n      return null;\n    }\n    return parsedAtom.schemeData;\n  }\n\n  /**\n   * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported.\n   *\n   * @param atom The atom to parse.\n   * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom\n   *     has an unsupported version.\n   */\n  // TODO: Support parsing of the key ids for version 1 PSSH atoms.\n  private static @Nullable PsshAtom parsePsshAtom(byte[] atom) {\n    ParsableByteArray atomData = new ParsableByteArray(atom);\n    if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) {\n      // Data too short.\n      return null;\n    }\n    atomData.setPosition(0);\n    int atomSize = atomData.readInt();\n    if (atomSize != atomData.bytesLeft() + 4) {\n      // Not an atom, or incorrect atom size.\n      return null;\n    }\n    int atomType = atomData.readInt();\n    if (atomType != Atom.TYPE_pssh) {\n      // Not an atom, or incorrect atom type.\n      return null;\n    }\n    int atomVersion = Atom.parseFullAtomVersion(atomData.readInt());\n    if (atomVersion > 1) {\n      Log.w(TAG, \"Unsupported pssh version: \" + atomVersion);\n      return null;\n    }\n    UUID uuid = new UUID(atomData.readLong(), atomData.readLong());\n    if (atomVersion == 1) {\n      int keyIdCount = atomData.readUnsignedIntToInt();\n      atomData.skipBytes(16 * keyIdCount);\n    }\n    int dataSize = atomData.readUnsignedIntToInt();\n    if (dataSize != atomData.bytesLeft()) {\n      // Incorrect dataSize.\n      return null;\n    }\n    byte[] data = new byte[dataSize];\n    atomData.readBytes(data, 0, dataSize);\n    return new PsshAtom(uuid, atomVersion, data);\n  }\n\n  // TODO: Consider exposing this and making parsePsshAtom public.\n  private static class PsshAtom {\n\n    private final UUID uuid;\n    private final int version;\n    private final byte[] schemeData;\n\n    public PsshAtom(UUID uuid, int version, byte[] schemeData) {\n      this.uuid = uuid;\n      this.version = version;\n      this.schemeData = schemeData;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * Provides methods that peek data from an {@link ExtractorInput} and return whether the input\n * appears to be in MP4 format.\n */\n/* package */ final class Sniffer {\n\n  /** The maximum number of bytes to peek when sniffing. */\n  private static final int SEARCH_LENGTH = 4 * 1024;\n\n  private static final int[] COMPATIBLE_BRANDS =\n      new int[] {\n        0x69736f6d, // isom\n        0x69736f32, // iso2\n        0x69736f33, // iso3\n        0x69736f34, // iso4\n        0x69736f35, // iso5\n        0x69736f36, // iso6\n        0x61766331, // avc1\n        0x68766331, // hvc1\n        0x68657631, // hev1\n        0x61763031, // av01\n        0x6d703431, // mp41\n        0x6d703432, // mp42\n        0x33673261, // 3g2a\n        0x33673262, // 3g2b\n        0x33677236, // 3gr6\n        0x33677336, // 3gs6\n        0x33676536, // 3ge6\n        0x33676736, // 3gg6\n        0x4d345620, // M4V[space]\n        0x4d344120, // M4A[space]\n        0x66347620, // f4v[space]\n        0x6b646469, // kddi\n        0x4d345650, // M4VP\n        0x71742020, // qt[space][space], Apple QuickTime\n        0x4d534e56, // MSNV, Sony PSP\n        0x64627931, // dby1, Dolby Vision\n      };\n\n  /**\n   * Returns whether data peeked from the current position in {@code input} is consistent with the\n   * input being a fragmented MP4 file.\n   *\n   * @param input The extractor input from which to peek data. The peek position will be modified.\n   * @return Whether the input appears to be in the fragmented MP4 format.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  public static boolean sniffFragmented(ExtractorInput input)\n      throws IOException, InterruptedException {\n    return sniffInternal(input, true);\n  }\n\n  /**\n   * Returns whether data peeked from the current position in {@code input} is consistent with the\n   * input being an unfragmented MP4 file.\n   *\n   * @param input The extractor input from which to peek data. The peek position will be modified.\n   * @return Whether the input appears to be in the unfragmented MP4 format.\n   * @throws IOException If an error occurs reading from the input.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  public static boolean sniffUnfragmented(ExtractorInput input)\n      throws IOException, InterruptedException {\n    return sniffInternal(input, false);\n  }\n\n  private static boolean sniffInternal(ExtractorInput input, boolean fragmented)\n      throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH\n        ? SEARCH_LENGTH : inputLength);\n\n    ParsableByteArray buffer = new ParsableByteArray(64);\n    int bytesSearched = 0;\n    boolean foundGoodFileType = false;\n    boolean isFragmented = false;\n    while (bytesSearched < bytesToSearch) {\n      // Read an atom header.\n      int headerSize = Atom.HEADER_SIZE;\n      buffer.reset(headerSize);\n      input.peekFully(buffer.data, 0, headerSize);\n      long atomSize = buffer.readUnsignedInt();\n      int atomType = buffer.readInt();\n      if (atomSize == Atom.DEFINES_LARGE_SIZE) {\n        // Read the large atom size.\n        headerSize = Atom.LONG_HEADER_SIZE;\n        input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);\n        buffer.setLimit(Atom.LONG_HEADER_SIZE);\n        atomSize = buffer.readLong();\n      } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {\n        // The atom extends to the end of the file.\n        long fileEndPosition = input.getLength();\n        if (fileEndPosition != C.LENGTH_UNSET) {\n          atomSize = fileEndPosition - input.getPeekPosition() + headerSize;\n        }\n      }\n\n      if (atomSize < headerSize) {\n        // The file is invalid because the atom size is too small for its header.\n        return false;\n      }\n      bytesSearched += headerSize;\n\n      if (atomType == Atom.TYPE_moov) {\n        // We have seen the moov atom. We increase the search size to make sure we don't miss an\n        // mvex atom because the moov's size exceeds the search length.\n        bytesToSearch += (int) atomSize;\n        if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) {\n          // Make sure we don't exceed the file size.\n          bytesToSearch = (int) inputLength;\n        }\n        // Check for an mvex atom inside the moov atom to identify whether the file is fragmented.\n        continue;\n      }\n\n      if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) {\n        // The movie is fragmented. Stop searching as we must have read any ftyp atom already.\n        isFragmented = true;\n        break;\n      }\n\n      if (bytesSearched + atomSize - headerSize >= bytesToSearch) {\n        // Stop searching as peeking this atom would exceed the search limit.\n        break;\n      }\n\n      int atomDataSize = (int) (atomSize - headerSize);\n      bytesSearched += atomDataSize;\n      if (atomType == Atom.TYPE_ftyp) {\n        // Parse the atom and check the file type/brand is compatible with the extractors.\n        if (atomDataSize < 8) {\n          return false;\n        }\n        buffer.reset(atomDataSize);\n        input.peekFully(buffer.data, 0, atomDataSize);\n        int brandsCount = atomDataSize / 4;\n        for (int i = 0; i < brandsCount; i++) {\n          if (i == 1) {\n            // This index refers to the minorVersion, not a brand, so skip it.\n            buffer.skipBytes(4);\n          } else if (isCompatibleBrand(buffer.readInt())) {\n            foundGoodFileType = true;\n            break;\n          }\n        }\n        if (!foundGoodFileType) {\n          // The types were not compatible and there is only one ftyp atom, so reject the file.\n          return false;\n        }\n      } else if (atomDataSize != 0) {\n        // Skip the atom.\n        input.advancePeekPosition(atomDataSize);\n      }\n    }\n    return foundGoodFileType && fragmented == isFragmented;\n  }\n\n  /**\n   * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors.\n   */\n  private static boolean isCompatibleBrand(int brand) {\n    // Accept all brands starting '3gp'.\n    if (brand >>> 8 == 0x00336770) {\n      return true;\n    }\n    for (int compatibleBrand : COMPATIBLE_BRANDS) {\n      if (compatibleBrand == brand) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private Sniffer() {\n    // Prevent instantiation.\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Encapsulates information describing an MP4 track.\n */\npublic final class Track {\n\n  /**\n   * The transformation to apply to samples in the track, if any. One of {@link\n   * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT})\n  public @interface Transformation {}\n  /**\n   * A no-op sample transformation.\n   */\n  public static final int TRANSFORMATION_NONE = 0;\n  /**\n   * A transformation for caption samples in cdat atoms.\n   */\n  public static final int TRANSFORMATION_CEA608_CDAT = 1;\n\n  /**\n   * The track identifier.\n   */\n  public final int id;\n\n  /**\n   * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.\n   */\n  public final int type;\n\n  /**\n   * The track timescale, defined as the number of time units that pass in one second.\n   */\n  public final long timescale;\n\n  /**\n   * The movie timescale.\n   */\n  public final long movieTimescale;\n\n  /**\n   * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown.\n   */\n  public final long durationUs;\n\n  /**\n   * The format.\n   */\n  public final Format format;\n\n  /**\n   * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each\n   * sample.\n   */\n  @Transformation public final int sampleTransformation;\n\n  /**\n   * Durations of edit list segments in the movie timescale. Null if there is no edit list.\n   */\n  @Nullable public final long[] editListDurations;\n\n  /**\n   * Media times for edit list segments in the track timescale. Null if there is no edit list.\n   */\n  @Nullable public final long[] editListMediaTimes;\n\n  /**\n   * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for\n   * other track types.\n   */\n  public final int nalUnitLengthFieldLength;\n\n  @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;\n\n  public Track(int id, int type, long timescale, long movieTimescale, long durationUs,\n      Format format, @Transformation int sampleTransformation,\n      @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,\n      @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) {\n    this.id = id;\n    this.type = type;\n    this.timescale = timescale;\n    this.movieTimescale = movieTimescale;\n    this.durationUs = durationUs;\n    this.format = format;\n    this.sampleTransformation = sampleTransformation;\n    this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;\n    this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;\n    this.editListDurations = editListDurations;\n    this.editListMediaTimes = editListMediaTimes;\n  }\n\n  /**\n   * Returns the {@link TrackEncryptionBox} for the given sample description index.\n   *\n   * @param sampleDescriptionIndex The given sample description index\n   * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no\n   *     such entry exists.\n   */\n  @Nullable\n  public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {\n    return sampleDescriptionEncryptionBoxes == null ? null\n        : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];\n  }\n\n  // incompatible types in argument.\n  @SuppressWarnings(\"nullness:argument.type.incompatible\")\n  public Track copyWithFormat(Format format) {\n    return new Track(\n        id,\n        type,\n        timescale,\n        movieTimescale,\n        durationUs,\n        format,\n        sampleTransformation,\n        sampleDescriptionEncryptionBoxes,\n        nalUnitLengthFieldLength,\n        editListDurations,\n        editListMediaTimes);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * Encapsulates information parsed from a track encryption (tenc) box or sample group description \n * (sgpd) box in an MP4 stream.\n */\npublic final class TrackEncryptionBox {\n\n  private static final String TAG = \"TrackEncryptionBox\";\n\n  /**\n   * Indicates the encryption state of the samples in the sample group.\n   */\n  public final boolean isEncrypted;\n\n  /**\n   * The protection scheme type, as defined by the 'schm' box, or null if unknown.\n   */\n  @Nullable public final String schemeType;\n\n  /**\n   * A {@link TrackOutput.CryptoData} instance containing the encryption information from this\n   * {@link TrackEncryptionBox}.\n   */\n  public final TrackOutput.CryptoData cryptoData;\n\n  /** The initialization vector size in bytes for the samples in the corresponding sample group. */\n  public final int perSampleIvSize;\n\n  /**\n   * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the\n   * track encryption box or sample group description box. Null otherwise.\n   */\n  @Nullable public final byte[] defaultInitializationVector;\n\n  /**\n   * @param isEncrypted See {@link #isEncrypted}.\n   * @param schemeType See {@link #schemeType}.\n   * @param perSampleIvSize See {@link #perSampleIvSize}.\n   * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}.\n   * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}.\n   * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}.\n   * @param defaultInitializationVector See {@link #defaultInitializationVector}.\n   */\n  public TrackEncryptionBox(\n      boolean isEncrypted,\n      @Nullable String schemeType,\n      int perSampleIvSize,\n      byte[] keyId,\n      int defaultEncryptedBlocks,\n      int defaultClearBlocks,\n      @Nullable byte[] defaultInitializationVector) {\n    Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null);\n    this.isEncrypted = isEncrypted;\n    this.schemeType = schemeType;\n    this.perSampleIvSize = perSampleIvSize;\n    this.defaultInitializationVector = defaultInitializationVector;\n    cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId,\n        defaultEncryptedBlocks, defaultClearBlocks);\n  }\n\n  @C.CryptoMode\n  private static int schemeToCryptoMode(@Nullable String schemeType) {\n    if (schemeType == null) {\n      // If unknown, assume cenc.\n      return C.CRYPTO_MODE_AES_CTR;\n    }\n    switch (schemeType) {\n      case C.CENC_TYPE_cenc:\n      case C.CENC_TYPE_cens:\n        return C.CRYPTO_MODE_AES_CTR;\n      case C.CENC_TYPE_cbc1:\n      case C.CENC_TYPE_cbcs:\n        return C.CRYPTO_MODE_AES_CBC;\n      default:\n        Log.w(TAG, \"Unsupported protection scheme type '\" + schemeType + \"'. Assuming AES-CTR \"\n            + \"crypto mode.\");\n        return C.CRYPTO_MODE_AES_CTR;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * A holder for information corresponding to a single fragment of an mp4 file.\n */\n/* package */ final class TrackFragment {\n\n  /**\n   * The default values for samples from the track fragment header.\n   */\n  public DefaultSampleValues header;\n  /**\n   * The position (byte offset) of the start of fragment.\n   */\n  public long atomPosition;\n  /**\n   * The position (byte offset) of the start of data contained in the fragment.\n   */\n  public long dataPosition;\n  /**\n   * The position (byte offset) of the start of auxiliary data.\n   */\n  public long auxiliaryDataPosition;\n  /**\n   * The number of track runs of the fragment.\n   */\n  public int trunCount;\n  /**\n   * The total number of samples in the fragment.\n   */\n  public int sampleCount;\n  /**\n   * The position (byte offset) of the start of sample data of each track run in the fragment.\n   */\n  public long[] trunDataPosition;\n  /**\n   * The number of samples contained by each track run in the fragment.\n   */\n  public int[] trunLength;\n  /**\n   * The size of each sample in the fragment.\n   */\n  public int[] sampleSizeTable;\n  /**\n   * The composition time offset of each sample in the fragment.\n   */\n  public int[] sampleCompositionTimeOffsetTable;\n  /**\n   * The decoding time of each sample in the fragment.\n   */\n  public long[] sampleDecodingTimeTable;\n  /**\n   * Indicates which samples are sync frames.\n   */\n  public boolean[] sampleIsSyncFrameTable;\n  /**\n   * Whether the fragment defines encryption data.\n   */\n  public boolean definesEncryptionData;\n  /**\n   * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.\n   * Undefined otherwise.\n   */\n  public boolean[] sampleHasSubsampleEncryptionTable;\n  /**\n   * Fragment specific track encryption. May be null.\n   */\n  public TrackEncryptionBox trackEncryptionBox;\n  /**\n   * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.\n   * Undefined otherwise.\n   */\n  public int sampleEncryptionDataLength;\n  /**\n   * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined\n   * otherwise.\n   */\n  public ParsableByteArray sampleEncryptionData;\n  /**\n   * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.\n   */\n  public boolean sampleEncryptionDataNeedsFill;\n  /**\n   * The absolute decode time of the start of the next fragment.\n   */\n  public long nextFragmentDecodeTime;\n\n  /**\n   * Resets the fragment.\n   * <p>\n   * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both\n   * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false,\n   * and {@link #trackEncryptionBox} is set to null.\n   */\n  public void reset() {\n    trunCount = 0;\n    nextFragmentDecodeTime = 0;\n    definesEncryptionData = false;\n    sampleEncryptionDataNeedsFill = false;\n    trackEncryptionBox = null;\n  }\n\n  /**\n   * Configures the fragment for the specified number of samples.\n   * <p>\n   * The {@link #sampleCount} of the fragment is set to the specified sample count, and the\n   * contained tables are resized if necessary such that they are at least this length.\n   *\n   * @param sampleCount The number of samples in the new run.\n   */\n  public void initTables(int trunCount, int sampleCount) {\n    this.trunCount = trunCount;\n    this.sampleCount = sampleCount;\n    if (trunLength == null || trunLength.length < trunCount) {\n      trunDataPosition = new long[trunCount];\n      trunLength = new int[trunCount];\n    }\n    if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) {\n      // Size the tables 25% larger than needed, so as to make future resize operations less\n      // likely. The choice of 25% is relatively arbitrary.\n      int tableSize = (sampleCount * 125) / 100;\n      sampleSizeTable = new int[tableSize];\n      sampleCompositionTimeOffsetTable = new int[tableSize];\n      sampleDecodingTimeTable = new long[tableSize];\n      sampleIsSyncFrameTable = new boolean[tableSize];\n      sampleHasSubsampleEncryptionTable = new boolean[tableSize];\n    }\n  }\n\n  /**\n   * Configures the fragment to be one that defines encryption data of the specified length.\n   * <p>\n   * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to\n   * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it\n   * is at least this length.\n   *\n   * @param length The length in bytes of the encryption data.\n   */\n  public void initEncryptionData(int length) {\n    if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {\n      sampleEncryptionData = new ParsableByteArray(length);\n    }\n    sampleEncryptionDataLength = length;\n    definesEncryptionData = true;\n    sampleEncryptionDataNeedsFill = true;\n  }\n\n  /**\n   * Fills {@link #sampleEncryptionData} from the provided input.\n   *\n   * @param input An {@link ExtractorInput} from which to read the encryption data.\n   */\n  public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException {\n    input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength);\n    sampleEncryptionData.setPosition(0);\n    sampleEncryptionDataNeedsFill = false;\n  }\n\n  /**\n   * Fills {@link #sampleEncryptionData} from the provided source.\n   *\n   * @param source A source from which to read the encryption data.\n   */\n  public void fillEncryptionData(ParsableByteArray source) {\n    source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);\n    sampleEncryptionData.setPosition(0);\n    sampleEncryptionDataNeedsFill = false;\n  }\n\n  public long getSamplePresentationTime(int index) {\n    return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];\n  }\n\n  /** Returns whether the sample at the given index has a subsample encryption table. */\n  public boolean sampleHasSubsampleEncryptionTable(int index) {\n    return definesEncryptionData && sampleHasSubsampleEncryptionTable[index];\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.mp4;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Sample table for a track in an MP4 file.\n */\n/* package */ final class TrackSampleTable {\n\n  /** The track corresponding to this sample table. */\n  public final Track track;\n  /** Number of samples. */\n  public final int sampleCount;\n  /** Sample offsets in bytes. */\n  public final long[] offsets;\n  /** Sample sizes in bytes. */\n  public final int[] sizes;\n  /** Maximum sample size in {@link #sizes}. */\n  public final int maximumSize;\n  /** Sample timestamps in microseconds. */\n  public final long[] timestampsUs;\n  /** Sample flags. */\n  public final int[] flags;\n  /**\n   * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample\n   * table is empty.\n   */\n  public final long durationUs;\n\n  public TrackSampleTable(\n      Track track,\n      long[] offsets,\n      int[] sizes,\n      int maximumSize,\n      long[] timestampsUs,\n      int[] flags,\n      long durationUs) {\n    Assertions.checkArgument(sizes.length == timestampsUs.length);\n    Assertions.checkArgument(offsets.length == timestampsUs.length);\n    Assertions.checkArgument(flags.length == timestampsUs.length);\n\n    this.track = track;\n    this.offsets = offsets;\n    this.sizes = sizes;\n    this.maximumSize = maximumSize;\n    this.timestampsUs = timestampsUs;\n    this.flags = flags;\n    this.durationUs = durationUs;\n    sampleCount = offsets.length;\n    if (flags.length > 0) {\n      flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;\n    }\n  }\n\n  /**\n   * Returns the sample index of the closest synchronization sample at or before the given\n   * timestamp, if one is available.\n   *\n   * @param timeUs Timestamp adjacent to which to find a synchronization sample.\n   * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.\n   */\n  public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {\n    // Video frame timestamps may not be sorted, so the behavior of this call can be undefined.\n    // Frames are not reordered past synchronization samples so this works in practice.\n    int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);\n    for (int i = startIndex; i >= 0; i--) {\n      if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns the sample index of the closest synchronization sample at or after the given timestamp,\n   * if one is available.\n   *\n   * @param timeUs Timestamp adjacent to which to find a synchronization sample.\n   * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.\n   */\n  public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {\n    int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);\n    for (int i = startIndex; i < timestampsUs.length; i++) {\n      if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/** Seeks in an Ogg stream. */\n/* package */ final class DefaultOggSeeker implements OggSeeker {\n\n  private static final int MATCH_RANGE = 72000;\n  private static final int MATCH_BYTE_RANGE = 100000;\n  private static final int DEFAULT_OFFSET = 30000;\n\n  private static final int STATE_SEEK_TO_END = 0;\n  private static final int STATE_READ_LAST_PAGE = 1;\n  private static final int STATE_SEEK = 2;\n  private static final int STATE_SKIP = 3;\n  private static final int STATE_IDLE = 4;\n\n  private final OggPageHeader pageHeader = new OggPageHeader();\n  private final long payloadStartPosition;\n  private final long payloadEndPosition;\n  private final StreamReader streamReader;\n\n  private int state;\n  private long totalGranules;\n  private long positionBeforeSeekToEnd;\n  private long targetGranule;\n\n  private long start;\n  private long end;\n  private long startGranule;\n  private long endGranule;\n\n  /**\n   * Constructs an OggSeeker.\n   *\n   * @param streamReader The {@link StreamReader} that owns this seeker.\n   * @param payloadStartPosition Start position of the payload (inclusive).\n   * @param payloadEndPosition End position of the payload (exclusive).\n   * @param firstPayloadPageSize The total size of the first payload page, in bytes.\n   * @param firstPayloadPageGranulePosition The granule position of the first payload page.\n   * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.\n   */\n  public DefaultOggSeeker(\n      StreamReader streamReader,\n      long payloadStartPosition,\n      long payloadEndPosition,\n      long firstPayloadPageSize,\n      long firstPayloadPageGranulePosition,\n      boolean firstPayloadPageIsLastPage) {\n    Assertions.checkArgument(\n        payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);\n    this.streamReader = streamReader;\n    this.payloadStartPosition = payloadStartPosition;\n    this.payloadEndPosition = payloadEndPosition;\n    if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition\n        || firstPayloadPageIsLastPage) {\n      totalGranules = firstPayloadPageGranulePosition;\n      state = STATE_IDLE;\n    } else {\n      state = STATE_SEEK_TO_END;\n    }\n  }\n\n  @Override\n  public long read(ExtractorInput input) throws IOException, InterruptedException {\n    switch (state) {\n      case STATE_IDLE:\n        return -1;\n      case STATE_SEEK_TO_END:\n        positionBeforeSeekToEnd = input.getPosition();\n        state = STATE_READ_LAST_PAGE;\n        // Seek to the end just before the last page of stream to get the duration.\n        long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;\n        if (lastPageSearchPosition > positionBeforeSeekToEnd) {\n          return lastPageSearchPosition;\n        }\n        // Fall through.\n      case STATE_READ_LAST_PAGE:\n        totalGranules = readGranuleOfLastPage(input);\n        state = STATE_IDLE;\n        return positionBeforeSeekToEnd;\n      case STATE_SEEK:\n        long position = getNextSeekPosition(input);\n        if (position != C.POSITION_UNSET) {\n          return position;\n        }\n        state = STATE_SKIP;\n        // Fall through.\n      case STATE_SKIP:\n        skipToPageOfTargetGranule(input);\n        state = STATE_IDLE;\n        return -(startGranule + 2);\n      default:\n        // Never happens.\n        throw new IllegalStateException();\n    }\n  }\n\n  @Override\n  public OggSeekMap createSeekMap() {\n    return totalGranules != 0 ? new OggSeekMap() : null;\n  }\n\n  @Override\n  public void startSeek(long targetGranule) {\n    this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);\n    state = STATE_SEEK;\n    start = payloadStartPosition;\n    end = payloadEndPosition;\n    startGranule = 0;\n    endGranule = totalGranules;\n  }\n\n  /**\n   * Performs a single step of a seeking binary search, returning the byte position from which data\n   * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.\n   * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be\n   * called to skip to the target page.\n   *\n   * @param input The {@link ExtractorInput} to read from.\n   * @return The byte position from which data should be provided for the next step, or {@link\n   *     C#POSITION_UNSET} if the search has converged.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {\n    if (start == end) {\n      return C.POSITION_UNSET;\n    }\n\n    long currentPosition = input.getPosition();\n    if (!skipToNextPage(input, end)) {\n      if (start == currentPosition) {\n        throw new IOException(\"No ogg page can be found.\");\n      }\n      return start;\n    }\n\n    pageHeader.populate(input, /* quiet= */ false);\n    input.resetPeekPosition();\n\n    long granuleDistance = targetGranule - pageHeader.granulePosition;\n    int pageSize = pageHeader.headerSize + pageHeader.bodySize;\n    if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {\n      return C.POSITION_UNSET;\n    }\n\n    if (granuleDistance < 0) {\n      end = currentPosition;\n      endGranule = pageHeader.granulePosition;\n    } else {\n      start = input.getPosition() + pageSize;\n      startGranule = pageHeader.granulePosition;\n    }\n\n    if (end - start < MATCH_BYTE_RANGE) {\n      end = start;\n      return start;\n    }\n\n    long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);\n    long nextPosition =\n        input.getPosition()\n            - offset\n            + (granuleDistance * (end - start) / (endGranule - startGranule));\n    return Util.constrainValue(nextPosition, start, end - 1);\n  }\n\n  /**\n   * Skips forward to the start of the page containing the {@code targetGranule}.\n   *\n   * @param input The {@link ExtractorInput} to read from.\n   * @throws ParserException If populating the page header fails.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  private void skipToPageOfTargetGranule(ExtractorInput input)\n      throws IOException, InterruptedException {\n    pageHeader.populate(input, /* quiet= */ false);\n    while (pageHeader.granulePosition <= targetGranule) {\n      input.skipFully(pageHeader.headerSize + pageHeader.bodySize);\n      start = input.getPosition();\n      startGranule = pageHeader.granulePosition;\n      pageHeader.populate(input, /* quiet= */ false);\n    }\n    input.resetPeekPosition();\n  }\n\n  /**\n   * Skips to the next page.\n   *\n   * @param input The {@code ExtractorInput} to skip to the next page.\n   * @throws IOException If peeking/reading from the input fails.\n   * @throws InterruptedException If the thread is interrupted.\n   * @throws EOFException If the next page can't be found before the end of the input.\n   */\n  @VisibleForTesting\n  void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {\n    if (!skipToNextPage(input, payloadEndPosition)) {\n      // Not found until eof.\n      throw new EOFException();\n    }\n  }\n\n  /**\n   * Skips to the next page. Searches for the next page header.\n   *\n   * @param input The {@code ExtractorInput} to skip to the next page.\n   * @param limit The limit up to which the search should take place.\n   * @return Whether the next page was found.\n   * @throws IOException If peeking/reading from the input fails.\n   * @throws InterruptedException If interrupted while peeking/reading from the input.\n   */\n  private boolean skipToNextPage(ExtractorInput input, long limit)\n      throws IOException, InterruptedException {\n    limit = Math.min(limit + 3, payloadEndPosition);\n    byte[] buffer = new byte[2048];\n    int peekLength = buffer.length;\n    while (true) {\n      if (input.getPosition() + peekLength > limit) {\n        // Make sure to not peek beyond the end of the input.\n        peekLength = (int) (limit - input.getPosition());\n        if (peekLength < 4) {\n          // Not found until end.\n          return false;\n        }\n      }\n      input.peekFully(buffer, 0, peekLength, false);\n      for (int i = 0; i < peekLength - 3; i++) {\n        if (buffer[i] == 'O'\n            && buffer[i + 1] == 'g'\n            && buffer[i + 2] == 'g'\n            && buffer[i + 3] == 'S') {\n          // Match! Skip to the start of the pattern.\n          input.skipFully(i);\n          return true;\n        }\n      }\n      // Overlap by not skipping the entire peekLength.\n      input.skipFully(peekLength - 3);\n    }\n  }\n\n  /**\n   * Skips to the last Ogg page in the stream and reads the header's granule field which is the\n   * total number of samples per channel.\n   *\n   * @param input The {@link ExtractorInput} to read from.\n   * @return The total number of samples of this input.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  @VisibleForTesting\n  long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {\n    skipToNextPage(input);\n    pageHeader.reset();\n    while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {\n      pageHeader.populate(input, /* quiet= */ false);\n      input.skipFully(pageHeader.headerSize + pageHeader.bodySize);\n    }\n    return pageHeader.granulePosition;\n  }\n\n  private final class OggSeekMap implements SeekMap {\n\n    @Override\n    public boolean isSeekable() {\n      return true;\n    }\n\n    @Override\n    public SeekPoints getSeekPoints(long timeUs) {\n      long targetGranule = streamReader.convertTimeToGranule(timeUs);\n      long estimatedPosition =\n          payloadStartPosition\n              + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)\n              - DEFAULT_OFFSET;\n      estimatedPosition =\n          Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);\n      return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));\n    }\n\n    @Override\n    public long getDurationUs() {\n      return streamReader.convertGranuleToTime(totalGranules);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.FlacStreamMetadata;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * {@link StreamReader} to extract Flac data out of Ogg byte stream.\n */\n/* package */ final class FlacReader extends StreamReader {\n\n  private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;\n  private static final byte SEEKTABLE_PACKET_TYPE = 0x03;\n\n  private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;\n\n  private FlacStreamMetadata streamMetadata;\n  private FlacOggSeeker flacOggSeeker;\n\n  public static boolean verifyBitstreamType(ParsableByteArray data) {\n    return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type\n        data.readUnsignedInt() == 0x464C4143; // ASCII signature \"FLAC\"\n  }\n\n  @Override\n  protected void reset(boolean headerData) {\n    super.reset(headerData);\n    if (headerData) {\n      streamMetadata = null;\n      flacOggSeeker = null;\n    }\n  }\n\n  private static boolean isAudioPacket(byte[] data) {\n    return data[0] == AUDIO_PACKET_TYPE;\n  }\n\n  @Override\n  protected long preparePayload(ParsableByteArray packet) {\n    if (!isAudioPacket(packet.data)) {\n      return -1;\n    }\n    return getFlacFrameBlockSize(packet);\n  }\n\n  @Override\n  protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {\n    byte[] data = packet.data;\n    if (streamMetadata == null) {\n      streamMetadata = new FlacStreamMetadata(data, 17);\n      int maxInputSize =\n          streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize;\n      byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());\n      metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks\n      List<byte[]> initializationData = Collections.singletonList(metadata);\n      setupData.format =\n          Format.createAudioSampleFormat(\n              /* id= */ null,\n              MimeTypes.AUDIO_FLAC,\n              /* codecs= */ null,\n              streamMetadata.bitRate(),\n              maxInputSize,\n              streamMetadata.channels,\n              streamMetadata.sampleRate,\n              initializationData,\n              /* drmInitData= */ null,\n              /* selectionFlags= */ 0,\n              /* language= */ null);\n    } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {\n      flacOggSeeker = new FlacOggSeeker();\n      flacOggSeeker.parseSeekTable(packet);\n    } else if (isAudioPacket(data)) {\n      if (flacOggSeeker != null) {\n        flacOggSeeker.setFirstFrameOffset(position);\n        setupData.oggSeeker = flacOggSeeker;\n      }\n      return false;\n    }\n    return true;\n  }\n\n  private int getFlacFrameBlockSize(ParsableByteArray packet) {\n    int blockSizeCode = (packet.data[2] & 0xFF) >> 4;\n    switch (blockSizeCode) {\n      case 1:\n        return 192;\n      case 2:\n      case 3:\n      case 4:\n      case 5:\n        return 576 << (blockSizeCode - 2);\n      case 6:\n      case 7:\n        // skip the sample number\n        packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);\n        packet.readUtf8EncodedLong();\n        int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort();\n        packet.setPosition(0);\n        return value + 1;\n      case 8:\n      case 9:\n      case 10:\n      case 11:\n      case 12:\n      case 13:\n      case 14:\n      case 15:\n        return 256 << (blockSizeCode - 8);\n      default:\n        return -1;\n    }\n  }\n\n  private class FlacOggSeeker implements OggSeeker, SeekMap {\n\n    private static final int METADATA_LENGTH_OFFSET = 1;\n    private static final int SEEK_POINT_SIZE = 18;\n\n    private long[] seekPointGranules;\n    private long[] seekPointOffsets;\n    private long firstFrameOffset;\n    private long pendingSeekGranule;\n\n    public FlacOggSeeker() {\n      firstFrameOffset = -1;\n      pendingSeekGranule = -1;\n    }\n\n    public void setFirstFrameOffset(long firstFrameOffset) {\n      this.firstFrameOffset = firstFrameOffset;\n    }\n\n    /**\n     * Parses a FLAC file seek table metadata structure and initializes internal fields.\n     *\n     * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its\n     *     position should be set to the beginning of the block.\n     * @see <a href=\"https://xiph.org/flac/format.html#metadata_block_seektable\">FLAC format\n     *     METADATA_BLOCK_SEEKTABLE</a>\n     */\n    public void parseSeekTable(ParsableByteArray data) {\n      data.skipBytes(METADATA_LENGTH_OFFSET);\n      int length = data.readUnsignedInt24();\n      int numberOfSeekPoints = length / SEEK_POINT_SIZE;\n      seekPointGranules = new long[numberOfSeekPoints];\n      seekPointOffsets = new long[numberOfSeekPoints];\n      for (int i = 0; i < numberOfSeekPoints; i++) {\n        seekPointGranules[i] = data.readLong();\n        seekPointOffsets[i] = data.readLong();\n        data.skipBytes(2); // Skip \"Number of samples in the target frame.\"\n      }\n    }\n\n    @Override\n    public long read(ExtractorInput input) throws IOException, InterruptedException {\n      if (pendingSeekGranule >= 0) {\n        long result = -(pendingSeekGranule + 2);\n        pendingSeekGranule = -1;\n        return result;\n      }\n      return -1;\n    }\n\n    @Override\n    public void startSeek(long targetGranule) {\n      int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);\n      pendingSeekGranule = seekPointGranules[index];\n    }\n\n    @Override\n    public SeekMap createSeekMap() {\n      return this;\n    }\n\n    @Override\n    public boolean isSeekable() {\n      return true;\n    }\n\n    @Override\n    public SeekPoints getSeekPoints(long timeUs) {\n      long granule = convertTimeToGranule(timeUs);\n      int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);\n      long seekTimeUs = convertGranuleToTime(seekPointGranules[index]);\n      long seekPosition = firstFrameOffset + seekPointOffsets[index];\n      SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);\n      if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) {\n        return new SeekPoints(seekPoint);\n      } else {\n        long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]);\n        long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1];\n        SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);\n        return new SeekPoints(seekPoint, secondSeekPoint);\n      }\n    }\n\n    @Override\n    public long getDurationUs() {\n      return streamMetadata.durationUs();\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * Extracts data from the Ogg container format.\n */\npublic class OggExtractor implements Extractor {\n\n  /** Factory for {@link OggExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()};\n\n  private static final int MAX_VERIFICATION_BYTES = 8;\n\n  private ExtractorOutput output;\n  private StreamReader streamReader;\n  private boolean streamReaderInitialized;\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    try {\n      return sniffInternal(input);\n    } catch (ParserException e) {\n      return false;\n    }\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.output = output;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    if (streamReader != null) {\n      streamReader.seek(position, timeUs);\n    }\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    if (streamReader == null) {\n      if (!sniffInternal(input)) {\n        throw new ParserException(\"Failed to determine bitstream type\");\n      }\n      input.resetPeekPosition();\n    }\n    if (!streamReaderInitialized) {\n      TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);\n      output.endTracks();\n      streamReader.init(output, trackOutput);\n      streamReaderInitialized = true;\n    }\n    return streamReader.read(input, seekPosition);\n  }\n\n  private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException {\n    OggPageHeader header = new OggPageHeader();\n    if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {\n      return false;\n    }\n\n    int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);\n    ParsableByteArray scratch = new ParsableByteArray(length);\n    input.peekFully(scratch.data, 0, length);\n\n    if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {\n      streamReader = new FlacReader();\n    } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {\n      streamReader = new VorbisReader();\n    } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {\n      streamReader = new OpusReader();\n    } else {\n      return false;\n    }\n    return true;\n  }\n\n  private static ParsableByteArray resetPosition(ParsableByteArray scratch) {\n    scratch.setPosition(0);\n    return scratch;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.util.Arrays;\n\n/**\n * OGG packet class.\n */\n/* package */ final class OggPacket {\n\n  private final OggPageHeader pageHeader = new OggPageHeader();\n  private final ParsableByteArray packetArray = new ParsableByteArray(\n      new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);\n\n  private int currentSegmentIndex = C.INDEX_UNSET;\n  private int segmentCount;\n  private boolean populated;\n\n  /**\n   * Resets this reader.\n   */\n  public void reset() {\n    pageHeader.reset();\n    packetArray.reset();\n    currentSegmentIndex = C.INDEX_UNSET;\n    populated = false;\n  }\n\n  /**\n   * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make\n   * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader\n   * can resume properly from an error while reading a continued packet spanned across multiple\n   * pages.\n   *\n   * @param input The {@link ExtractorInput} to read data from.\n   * @return {@code true} if the read was successful. The read fails if the end of the input is\n   *     encountered without reading data.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public boolean populate(ExtractorInput input) throws IOException, InterruptedException {\n    Assertions.checkState(input != null);\n\n    if (populated) {\n      populated = false;\n      packetArray.reset();\n    }\n\n    while (!populated) {\n      if (currentSegmentIndex < 0) {\n        // We're at the start of a page.\n        if (!pageHeader.populate(input, true)) {\n          return false;\n        }\n        int segmentIndex = 0;\n        int bytesToSkip = pageHeader.headerSize;\n        if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {\n          // After seeking, the first packet may be the remainder\n          // part of a continued packet which has to be discarded.\n          bytesToSkip += calculatePacketSize(segmentIndex);\n          segmentIndex += segmentCount;\n        }\n        input.skipFully(bytesToSkip);\n        currentSegmentIndex = segmentIndex;\n      }\n\n      int size = calculatePacketSize(currentSegmentIndex);\n      int segmentIndex = currentSegmentIndex + segmentCount;\n      if (size > 0) {\n        if (packetArray.capacity() < packetArray.limit() + size) {\n          packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size);\n        }\n        input.readFully(packetArray.data, packetArray.limit(), size);\n        packetArray.setLimit(packetArray.limit() + size);\n        populated = pageHeader.laces[segmentIndex - 1] != 255;\n      }\n      // Advance now since we are sure reading didn't throw an exception.\n      currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET\n          : segmentIndex;\n    }\n    return true;\n  }\n\n  /**\n   * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,\n   * or an empty header if the packet has yet to be populated.\n   *\n   * <p>Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent\n   * calls to {@link #populate(ExtractorInput)}.\n   *\n   * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet\n   *     to be populated.\n   */\n  public OggPageHeader getPageHeader() {\n    return pageHeader;\n  }\n\n  /**\n   * Returns a {@link ParsableByteArray} containing the packet's payload.\n   */\n  public ParsableByteArray getPayload() {\n    return packetArray;\n  }\n\n  /**\n   * Trims the packet data array.\n   */\n  public void trimPayload() {\n    if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) {\n      return;\n    }\n    packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD,\n        packetArray.limit()));\n  }\n\n  /**\n   * Calculates the size of the packet starting from {@code startSegmentIndex}.\n   *\n   * @param startSegmentIndex the index of the first segment of the packet.\n   * @return Size of the packet.\n   */\n  private int calculatePacketSize(int startSegmentIndex) {\n    segmentCount = 0;\n    int size = 0;\n    while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {\n      int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];\n      size += segmentLength;\n      if (segmentLength != 255) {\n        // packets end at first lace < 255\n        break;\n      }\n    }\n    return size;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/**\n * Data object to store header information.\n */\n/* package */  final class OggPageHeader {\n\n  public static final int EMPTY_PAGE_HEADER_SIZE = 27;\n  public static final int MAX_SEGMENT_COUNT = 255;\n  public static final int MAX_PAGE_PAYLOAD = 255 * 255;\n  public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT\n      + MAX_PAGE_PAYLOAD;\n\n  private static final int TYPE_OGGS = 0x4f676753;\n\n  public int revision;\n  public int type;\n  /**\n   * The absolute granule position of the page. This is the total number of samples from the start\n   * of the file up to the <em>end</em> of the page. Samples partially in the page that continue on\n   * the next page do not count.\n   */\n  public long granulePosition;\n\n  public long streamSerialNumber;\n  public long pageSequenceNumber;\n  public long pageChecksum;\n  public int pageSegmentCount;\n  public int headerSize;\n  public int bodySize;\n  /**\n   * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use\n   * {@link #pageSegmentCount} to iterate.\n   */\n  public final int[] laces = new int[MAX_SEGMENT_COUNT];\n\n  private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);\n\n  /**\n   * Resets all primitive member fields to zero.\n   */\n  public void reset() {\n    revision = 0;\n    type = 0;\n    granulePosition = 0;\n    streamSerialNumber = 0;\n    pageSequenceNumber = 0;\n    pageChecksum = 0;\n    pageSegmentCount = 0;\n    headerSize = 0;\n    bodySize = 0;\n  }\n\n  /**\n   * Peeks an Ogg page header and updates this {@link OggPageHeader}.\n   *\n   * @param input The {@link ExtractorInput} to read from.\n   * @param quiet Whether to return {@code false} rather than throwing an exception if the header\n   *     cannot be populated.\n   * @return Whether the read was successful. The read fails if the end of the input is encountered\n   *     without reading data.\n   * @throws IOException If reading data fails or the stream is invalid.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public boolean populate(ExtractorInput input, boolean quiet)\n      throws IOException, InterruptedException {\n    scratch.reset();\n    reset();\n    boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET\n        || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;\n    if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new EOFException();\n      }\n    }\n    if (scratch.readUnsignedInt() != TYPE_OGGS) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new ParserException(\"expected OggS capture pattern at begin of page\");\n      }\n    }\n\n    revision = scratch.readUnsignedByte();\n    if (revision != 0x00) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new ParserException(\"unsupported bit stream revision\");\n      }\n    }\n    type = scratch.readUnsignedByte();\n\n    granulePosition = scratch.readLittleEndianLong();\n    streamSerialNumber = scratch.readLittleEndianUnsignedInt();\n    pageSequenceNumber = scratch.readLittleEndianUnsignedInt();\n    pageChecksum = scratch.readLittleEndianUnsignedInt();\n    pageSegmentCount = scratch.readUnsignedByte();\n    headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;\n\n    // calculate total size of header including laces\n    scratch.reset();\n    input.peekFully(scratch.data, 0, pageSegmentCount);\n    for (int i = 0; i < pageSegmentCount; i++) {\n      laces[i] = scratch.readUnsignedByte();\n      bodySize += laces[i];\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport java.io.IOException;\n\n/**\n * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive\n * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position\n * and start the seeking with an initial estimated position.\n */\n/* package */ interface OggSeeker {\n\n  /**\n   * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking\n   * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1.\n   */\n  SeekMap createSeekMap();\n\n  /**\n   * Starts a seek operation.\n   *\n   * @param targetGranule The target granule position.\n   */\n  void startSeek(long targetGranule);\n\n  /**\n   * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.\n   * <p/>\n   * If more data is required or if the position of the input needs to be modified then a position\n   * from which data should be provided is returned. Else a negative value is returned. If a seek\n   * has been completed then the value returned is -(currentGranule + 2). Else it is -1.\n   *\n   * @param input The {@link ExtractorInput} to read from.\n   * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2)\n   *     if the progressive seek has completed, or -1 otherwise.\n   * @throws IOException If reading from the {@link ExtractorInput} fails.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  long read(ExtractorInput input) throws IOException, InterruptedException;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * {@link StreamReader} to extract Opus data out of Ogg byte stream.\n */\n/* package */ final class OpusReader extends StreamReader {\n\n  private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;\n\n  /**\n   * Opus streams are always decoded at 48000 Hz.\n   */\n  private static final int SAMPLE_RATE = 48000;\n\n  private static final int OPUS_CODE = 0x4f707573;\n  private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};\n\n  private boolean headerRead;\n\n  public static boolean verifyBitstreamType(ParsableByteArray data) {\n    if (data.bytesLeft() < OPUS_SIGNATURE.length) {\n      return false;\n    }\n    byte[] header = new byte[OPUS_SIGNATURE.length];\n    data.readBytes(header, 0, OPUS_SIGNATURE.length);\n    return Arrays.equals(header, OPUS_SIGNATURE);\n  }\n\n  @Override\n  protected void reset(boolean headerData) {\n    super.reset(headerData);\n    if (headerData) {\n      headerRead = false;\n    }\n  }\n\n  @Override\n  protected long preparePayload(ParsableByteArray packet) {\n    return convertTimeToGranule(getPacketDurationUs(packet.data));\n  }\n\n  @Override\n  protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {\n    if (!headerRead) {\n      byte[] metadata = Arrays.copyOf(packet.data, packet.limit());\n      int channelCount = metadata[9] & 0xFF;\n      int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);\n\n      List<byte[]> initializationData = new ArrayList<>(3);\n      initializationData.add(metadata);\n      putNativeOrderLong(initializationData, preskip);\n      putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);\n\n      setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null,\n          Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0,\n          null);\n      headerRead = true;\n    } else {\n      boolean headerPacket = packet.readInt() == OPUS_CODE;\n      packet.setPosition(0);\n      return headerPacket;\n    }\n    return true;\n  }\n\n  private void putNativeOrderLong(List<byte[]> initializationData, int samples) {\n    long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;\n    byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();\n    initializationData.add(array);\n  }\n\n  /**\n   * Returns the duration of the given audio packet.\n   *\n   * @param packet Contains audio data.\n   * @return Returns the duration of the given audio packet.\n   */\n  private long getPacketDurationUs(byte[] packet) {\n    int toc = packet[0] & 0xFF;\n    int frames;\n    switch (toc & 0x3) {\n      case 0:\n        frames = 1;\n        break;\n      case 1:\n      case 2:\n        frames = 2;\n        break;\n      default:\n        frames = packet[1] & 0x3F;\n        break;\n    }\n\n    int config = toc >> 3;\n    int length = config & 0x3;\n    if (config >= 16) {\n      length = 2500 << length;\n    } else if (config >= 12) {\n      length = 10000 << (length & 0x1);\n    } else if (length == 3) {\n      length = 60000;\n    } else {\n      length = 10000 << length;\n    }\n    return (long) frames * length;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/** StreamReader abstract class. */\n@SuppressWarnings(\"UngroupedOverloads\")\n/* package */ abstract class StreamReader {\n\n  private static final int STATE_READ_HEADERS = 0;\n  private static final int STATE_SKIP_HEADERS = 1;\n  private static final int STATE_READ_PAYLOAD = 2;\n  private static final int STATE_END_OF_INPUT = 3;\n\n  static class SetupData {\n    Format format;\n    OggSeeker oggSeeker;\n  }\n\n  private final OggPacket oggPacket;\n\n  private TrackOutput trackOutput;\n  private ExtractorOutput extractorOutput;\n  private OggSeeker oggSeeker;\n  private long targetGranule;\n  private long payloadStartPosition;\n  private long currentGranule;\n  private int state;\n  private int sampleRate;\n  private SetupData setupData;\n  private long lengthOfReadPacket;\n  private boolean seekMapSet;\n  private boolean formatSet;\n\n  public StreamReader() {\n    oggPacket = new OggPacket();\n  }\n\n  void init(ExtractorOutput output, TrackOutput trackOutput) {\n    this.extractorOutput = output;\n    this.trackOutput = trackOutput;\n    reset(true);\n  }\n\n  /**\n   * Resets the state of the {@link StreamReader}.\n   *\n   * @param headerData Resets parsed header data too.\n   */\n  protected void reset(boolean headerData) {\n    if (headerData) {\n      setupData = new SetupData();\n      payloadStartPosition = 0;\n      state = STATE_READ_HEADERS;\n    } else {\n      state = STATE_SKIP_HEADERS;\n    }\n    targetGranule = -1;\n    currentGranule = 0;\n  }\n\n  /**\n   * @see Extractor#seek(long, long)\n   */\n  final void seek(long position, long timeUs) {\n    oggPacket.reset();\n    if (position == 0) {\n      reset(!seekMapSet);\n    } else {\n      if (state != STATE_READ_HEADERS) {\n        targetGranule = convertTimeToGranule(timeUs);\n        oggSeeker.startSeek(targetGranule);\n        state = STATE_READ_PAYLOAD;\n      }\n    }\n  }\n\n  /**\n   * @see Extractor#read(ExtractorInput, PositionHolder)\n   */\n  final int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    switch (state) {\n      case STATE_READ_HEADERS:\n        return readHeaders(input);\n      case STATE_SKIP_HEADERS:\n        input.skipFully((int) payloadStartPosition);\n        state = STATE_READ_PAYLOAD;\n        return Extractor.RESULT_CONTINUE;\n      case STATE_READ_PAYLOAD:\n        return readPayload(input, seekPosition);\n      default:\n        // Never happens.\n        throw new IllegalStateException();\n    }\n  }\n\n  private int readHeaders(ExtractorInput input) throws IOException, InterruptedException {\n    boolean readingHeaders = true;\n    while (readingHeaders) {\n      if (!oggPacket.populate(input)) {\n        state = STATE_END_OF_INPUT;\n        return Extractor.RESULT_END_OF_INPUT;\n      }\n      lengthOfReadPacket = input.getPosition() - payloadStartPosition;\n\n      readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);\n      if (readingHeaders) {\n        payloadStartPosition = input.getPosition();\n      }\n    }\n\n    sampleRate = setupData.format.sampleRate;\n    if (!formatSet) {\n      trackOutput.format(setupData.format);\n      formatSet = true;\n    }\n\n    if (setupData.oggSeeker != null) {\n      oggSeeker = setupData.oggSeeker;\n    } else if (input.getLength() == C.LENGTH_UNSET) {\n      oggSeeker = new UnseekableOggSeeker();\n    } else {\n      OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader();\n      boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.\n      oggSeeker =\n          new DefaultOggSeeker(\n              this,\n              payloadStartPosition,\n              input.getLength(),\n              firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,\n              firstPayloadPageHeader.granulePosition,\n              isLastPage);\n    }\n\n    setupData = null;\n    state = STATE_READ_PAYLOAD;\n    // First payload packet. Trim the payload array of the ogg packet after headers have been read.\n    oggPacket.trimPayload();\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private int readPayload(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    long position = oggSeeker.read(input);\n    if (position >= 0) {\n      seekPosition.position = position;\n      return Extractor.RESULT_SEEK;\n    } else if (position < -1) {\n      onSeekEnd(-(position + 2));\n    }\n    if (!seekMapSet) {\n      SeekMap seekMap = oggSeeker.createSeekMap();\n      extractorOutput.seekMap(seekMap);\n      seekMapSet = true;\n    }\n\n    if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {\n      lengthOfReadPacket = 0;\n      ParsableByteArray payload = oggPacket.getPayload();\n      long granulesInPacket = preparePayload(payload);\n      if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {\n        // calculate time and send payload data to codec\n        long timeUs = convertGranuleToTime(currentGranule);\n        trackOutput.sampleData(payload, payload.limit());\n        trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);\n        targetGranule = -1;\n      }\n      currentGranule += granulesInPacket;\n    } else {\n      state = STATE_END_OF_INPUT;\n      return Extractor.RESULT_END_OF_INPUT;\n    }\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  /**\n   * Converts granule value to time.\n   *\n   * @param granule The granule value.\n   * @return Time in milliseconds.\n   */\n  protected long convertGranuleToTime(long granule) {\n    return (granule * C.MICROS_PER_SECOND) / sampleRate;\n  }\n\n  /**\n   * Converts time value to granule.\n   *\n   * @param timeUs Time in milliseconds.\n   * @return The granule value.\n   */\n  protected long convertTimeToGranule(long timeUs) {\n    return (sampleRate * timeUs) / C.MICROS_PER_SECOND;\n  }\n\n  /**\n   * Prepares payload data in the packet for submitting to TrackOutput and returns number of\n   * granules in the packet.\n   *\n   * @param packet Ogg payload data packet.\n   * @return Number of granules in the packet or -1 if the packet doesn't contain payload data.\n   */\n  protected abstract long preparePayload(ParsableByteArray packet);\n\n  /**\n   * Checks if the given packet is a header packet and reads it.\n   *\n   * @param packet An ogg packet.\n   * @param position Position of the given header packet.\n   * @param setupData Setup data to be filled.\n   * @return Whether the packet contains header data.\n   */\n  protected abstract boolean readHeaders(ParsableByteArray packet, long position,\n      SetupData setupData) throws IOException, InterruptedException;\n\n  /**\n   * Called on end of seeking.\n   *\n   * @param currentGranule The granule at the current input position.\n   */\n  protected void onSeekEnd(long currentGranule) {\n    this.currentGranule = currentGranule;\n  }\n\n  private static final class UnseekableOggSeeker implements OggSeeker {\n\n    @Override\n    public long read(ExtractorInput input) {\n      return -1;\n    }\n\n    @Override\n    public void startSeek(long targetGranule) {\n      // Do nothing.\n    }\n\n    @Override\n    public SeekMap createSeekMap() {\n      return new SeekMap.Unseekable(C.TIME_UNSET);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream.\n *\n * @see <a href=\"https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002\">Vorbis bitpacking\n *     specification</a>\n */\n/* package */ final class VorbisBitArray {\n\n  private final byte[] data;\n  private final int byteLimit;\n\n  private int byteOffset;\n  private int bitOffset;\n\n  /**\n   * Creates a new instance that wraps an existing array.\n   *\n   * @param data the array to wrap.\n   */\n  public VorbisBitArray(byte[] data) {\n    this.data = data;\n    byteLimit = data.length;\n  }\n\n  /**\n   * Resets the reading position to zero.\n   */\n  public void reset() {\n    byteOffset = 0;\n    bitOffset = 0;\n  }\n\n  /**\n   * Reads a single bit.\n   *\n   * @return {@code true} if the bit is set, {@code false} otherwise.\n   */\n  public boolean readBit() {\n    boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1;\n    skipBits(1);\n    return returnValue;\n  }\n\n  /**\n   * Reads up to 32 bits.\n   *\n   * @param numBits The number of bits to read.\n   * @return An integer whose bottom {@code numBits} bits hold the read data.\n   */\n  public int readBits(int numBits) {\n    int tempByteOffset = byteOffset;\n    int bitsRead = Math.min(numBits, 8 - bitOffset);\n    int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead));\n    while (bitsRead < numBits) {\n      returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead;\n      bitsRead += 8;\n    }\n    returnValue &= 0xFFFFFFFF >>> (32 - numBits);\n    skipBits(numBits);\n    return returnValue;\n  }\n\n  /**\n   * Skips {@code numberOfBits} bits.\n   *\n   * @param numBits The number of bits to skip.\n   */\n  public void skipBits(int numBits) {\n    int numBytes = numBits / 8;\n    byteOffset += numBytes;\n    bitOffset += numBits - (numBytes * 8);\n    if (bitOffset > 7) {\n      byteOffset++;\n      bitOffset -= 8;\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Returns the reading position in bits.\n   */\n  public int getPosition() {\n    return byteOffset * 8 + bitOffset;\n  }\n\n  /**\n   * Sets the reading position in bits.\n   *\n   * @param position The new reading position in bits.\n   */\n  public void setPosition(int position) {\n    byteOffset = position / 8;\n    bitOffset = position - (byteOffset * 8);\n    assertValidOffset();\n  }\n\n  /**\n   * Returns the number of remaining bits.\n   */\n  public int bitsLeft() {\n    return (byteLimit - byteOffset) * 8 - bitOffset;\n  }\n\n  private void assertValidOffset() {\n    // It is fine for position to be at the end of the array, but no further.\n    Assertions.checkState(byteOffset >= 0\n        && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.util.ArrayList;\n\n/**\n * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.\n */\n/* package */ final class VorbisReader extends StreamReader {\n\n  private VorbisSetup vorbisSetup;\n  private int previousPacketBlockSize;\n  private boolean seenFirstAudioPacket;\n\n  private VorbisUtil.VorbisIdHeader vorbisIdHeader;\n  private VorbisUtil.CommentHeader commentHeader;\n\n  public static boolean verifyBitstreamType(ParsableByteArray data) {\n    try {\n      return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);\n    } catch (ParserException e) {\n      return false;\n    }\n  }\n\n  @Override\n  protected void reset(boolean headerData) {\n    super.reset(headerData);\n    if (headerData) {\n      vorbisSetup = null;\n      vorbisIdHeader = null;\n      commentHeader = null;\n    }\n    previousPacketBlockSize = 0;\n    seenFirstAudioPacket = false;\n  }\n\n  @Override\n  protected void onSeekEnd(long currentGranule) {\n    super.onSeekEnd(currentGranule);\n    seenFirstAudioPacket = currentGranule != 0;\n    previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;\n  }\n\n  @Override\n  protected long preparePayload(ParsableByteArray packet) {\n    // if this is not an audio packet...\n    if ((packet.data[0] & 0x01) == 1) {\n      return -1;\n    }\n\n    // ... we need to decode the block size\n    int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);\n    // a packet contains samples produced from overlapping the previous and current frame data\n    // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)\n    int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4\n        : 0;\n    // codec expects the number of samples appended to audio data\n    appendNumberOfSamples(packet, samplesInPacket);\n\n    // update state in members for next iteration\n    seenFirstAudioPacket = true;\n    previousPacketBlockSize = packetBlockSize;\n    return samplesInPacket;\n  }\n\n  @Override\n  protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)\n      throws IOException, InterruptedException {\n    if (vorbisSetup != null) {\n      return false;\n    }\n\n    vorbisSetup = readSetupHeaders(packet);\n    if (vorbisSetup == null) {\n      return true;\n    }\n\n    ArrayList<byte[]> codecInitialisationData = new ArrayList<>();\n    codecInitialisationData.add(vorbisSetup.idHeader.data);\n    codecInitialisationData.add(vorbisSetup.setupHeaderData);\n\n    setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,\n        this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,\n        this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,\n        codecInitialisationData, null, 0, null);\n    return true;\n  }\n\n  @VisibleForTesting\n  /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException {\n\n    if (vorbisIdHeader == null) {\n      vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);\n      return null;\n    }\n\n    if (commentHeader == null) {\n      commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);\n      return null;\n    }\n\n    // the third packet contains the setup header\n    byte[] setupHeaderData = new byte[scratch.limit()];\n    // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2\n    System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());\n    // partially decode setup header to get the modes\n    Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);\n    // we need the ilog of modes all the time when extracting, so we compute it once\n    int iLogModes = VorbisUtil.iLog(modes.length - 1);\n\n    return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);\n  }\n\n  /**\n   * Reads an int of {@code length} bits from {@code src} starting at {@code\n   * leastSignificantBitIndex}.\n   *\n   * @param src the {@code byte} to read from.\n   * @param length the length in bits of the int to read.\n   * @param leastSignificantBitIndex the index of the least significant bit of the int to read.\n   * @return the int value read.\n   */\n  @VisibleForTesting\n  /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {\n    return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));\n  }\n\n  @VisibleForTesting\n  /* package */ static void appendNumberOfSamples(\n      ParsableByteArray buffer, long packetSampleCount) {\n\n    buffer.setLimit(buffer.limit() + 4);\n    // The vorbis decoder expects the number of samples in the packet\n    // to be appended to the audio data as an int32\n    buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF);\n    buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);\n    buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);\n    buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);\n  }\n\n  private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {\n    // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)\n    int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);\n    int currentBlockSize;\n    if (!vorbisSetup.modes[modeNumber].blockFlag) {\n      currentBlockSize = vorbisSetup.idHeader.blockSize0;\n    } else {\n      currentBlockSize = vorbisSetup.idHeader.blockSize1;\n    }\n    return currentBlockSize;\n  }\n\n  /**\n   * Class to hold all data read from Vorbis setup headers.\n   */\n  /* package */ static final class VorbisSetup {\n\n    public final VorbisUtil.VorbisIdHeader idHeader;\n    public final VorbisUtil.CommentHeader commentHeader;\n    public final byte[] setupHeaderData;\n    public final Mode[] modes;\n    public final int iLogModes;\n\n    public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader\n        commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {\n      this.idHeader = idHeader;\n      this.commentHeader = commentHeader;\n      this.setupHeaderData = setupHeaderData;\n      this.modes = modes;\n      this.iLogModes = iLogModes;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ogg;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Arrays;\n\n/**\n * Utility methods for parsing vorbis streams.\n */\n/* package */ final class VorbisUtil {\n\n  private static final String TAG = \"VorbisUtil\";\n\n  /**\n   * Returns ilog(x), which is the index of the highest set bit in {@code x}.\n   *\n   * @see <a href=\"https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1\">\n   *     Vorbis spec</a>\n   * @param x the value of which the ilog should be calculated.\n   * @return ilog(x)\n   */\n  public static int iLog(int x) {\n    int val = 0;\n    while (x > 0) {\n      val++;\n      x >>>= 1;\n    }\n    return val;\n  }\n\n  /**\n   * Reads a vorbis identification header from {@code headerData}.\n   *\n   * @see <a href=\"https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2\">Vorbis\n   *     spec/Identification header</a>\n   * @param headerData a {@link ParsableByteArray} wrapping the header data.\n   * @return a {@link VorbisIdHeader} with meta data.\n   * @throws ParserException thrown if invalid capture pattern is detected.\n   */\n  public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)\n      throws ParserException {\n\n    verifyVorbisHeaderCapturePattern(0x01, headerData, false);\n\n    long version = headerData.readLittleEndianUnsignedInt();\n    int channels = headerData.readUnsignedByte();\n    long sampleRate = headerData.readLittleEndianUnsignedInt();\n    int bitrateMax = headerData.readLittleEndianInt();\n    int bitrateNominal = headerData.readLittleEndianInt();\n    int bitrateMin = headerData.readLittleEndianInt();\n\n    int blockSize = headerData.readUnsignedByte();\n    int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);\n    int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);\n\n    boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;\n    // raw data of vorbis setup header has to be passed to decoder as CSD buffer #1\n    byte[] data = Arrays.copyOf(headerData.data, headerData.limit());\n\n    return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,\n        blockSize0, blockSize1, framingFlag, data);\n  }\n\n  /**\n   * Reads a vorbis comment header.\n   *\n   * @see <a href=\"https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3\">\n   *     Vorbis spec/Comment header</a>\n   * @param headerData a {@link ParsableByteArray} wrapping the header data.\n   * @return a {@link CommentHeader} with all the comments.\n   * @throws ParserException thrown if invalid capture pattern is detected.\n   */\n  public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)\n      throws ParserException {\n\n    verifyVorbisHeaderCapturePattern(0x03, headerData, false);\n    int length = 7;\n\n    int len = (int) headerData.readLittleEndianUnsignedInt();\n    length += 4;\n    String vendor = headerData.readString(len);\n    length += vendor.length();\n\n    long commentListLen = headerData.readLittleEndianUnsignedInt();\n    String[] comments = new String[(int) commentListLen];\n    length += 4;\n    for (int i = 0; i < commentListLen; i++) {\n      len = (int) headerData.readLittleEndianUnsignedInt();\n      length += 4;\n      comments[i] = headerData.readString(len);\n      length += comments[i].length();\n    }\n    if ((headerData.readUnsignedByte() & 0x01) == 0) {\n      throw new ParserException(\"framing bit expected to be set\");\n    }\n    length += 1;\n    return new CommentHeader(vendor, comments, length);\n  }\n\n  /**\n   * Verifies whether the next bytes in {@code header} are a vorbis header of the given\n   * {@code headerType}.\n   *\n   * @param headerType the type of the header expected.\n   * @param header the alleged header bytes.\n   * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.\n   * @return the number of bytes read.\n   * @throws ParserException thrown if header type or capture pattern is not as expected.\n   */\n  public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,\n      boolean quiet)\n      throws ParserException {\n    if (header.bytesLeft() < 7) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new ParserException(\"too short header: \" + header.bytesLeft());\n      }\n    }\n\n    if (header.readUnsignedByte() != headerType) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new ParserException(\"expected header type \" + Integer.toHexString(headerType));\n      }\n    }\n\n    if (!(header.readUnsignedByte() == 'v'\n        && header.readUnsignedByte() == 'o'\n        && header.readUnsignedByte() == 'r'\n        && header.readUnsignedByte() == 'b'\n        && header.readUnsignedByte() == 'i'\n        && header.readUnsignedByte() == 's')) {\n      if (quiet) {\n        return false;\n      } else {\n        throw new ParserException(\"expected characters 'vorbis'\");\n      }\n    }\n    return true;\n  }\n\n  /**\n   * This method reads the modes which are located at the very end of the vorbis setup header.\n   * That's why we need to partially decode or at least read the entire setup header to know\n   * where to start reading the modes.\n   *\n   * @see <a href=\"https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4\">\n   *     Vorbis spec/Setup header</a>\n   * @param headerData a {@link ParsableByteArray} containing setup header data.\n   * @param channels the number of channels.\n   * @return an array of {@link Mode}s.\n   * @throws ParserException thrown if bit stream is invalid.\n   */\n  public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)\n      throws ParserException {\n\n    verifyVorbisHeaderCapturePattern(0x05, headerData, false);\n\n    int numberOfBooks = headerData.readUnsignedByte() + 1;\n\n    VorbisBitArray bitArray  = new VorbisBitArray(headerData.data);\n    bitArray.skipBits(headerData.getPosition() * 8);\n\n    for (int i = 0; i < numberOfBooks; i++) {\n      readBook(bitArray);\n    }\n\n    int timeCount = bitArray.readBits(6) + 1;\n    for (int i = 0; i < timeCount; i++) {\n      if (bitArray.readBits(16) != 0x00) {\n        throw new ParserException(\"placeholder of time domain transforms not zeroed out\");\n      }\n    }\n    readFloors(bitArray);\n    readResidues(bitArray);\n    readMappings(channels, bitArray);\n\n    Mode[] modes = readModes(bitArray);\n    if (!bitArray.readBit()) {\n      throw new ParserException(\"framing bit after modes not set as expected\");\n    }\n    return modes;\n  }\n\n  private static Mode[] readModes(VorbisBitArray bitArray) {\n    int modeCount = bitArray.readBits(6) + 1;\n    Mode[] modes = new Mode[modeCount];\n    for (int i = 0; i < modeCount; i++) {\n      boolean blockFlag = bitArray.readBit();\n      int windowType = bitArray.readBits(16);\n      int transformType = bitArray.readBits(16);\n      int mapping = bitArray.readBits(8);\n      modes[i] = new Mode(blockFlag, windowType, transformType, mapping);\n    }\n    return modes;\n  }\n\n  private static void readMappings(int channels, VorbisBitArray bitArray)\n      throws ParserException {\n    int mappingsCount = bitArray.readBits(6) + 1;\n    for (int i = 0; i < mappingsCount; i++) {\n      int mappingType = bitArray.readBits(16);\n      if (mappingType != 0) {\n        Log.e(TAG, \"mapping type other than 0 not supported: \" + mappingType);\n        continue;\n      }\n      int submaps;\n      if (bitArray.readBit()) {\n        submaps = bitArray.readBits(4) + 1;\n      } else {\n        submaps = 1;\n      }\n      int couplingSteps;\n      if (bitArray.readBit()) {\n        couplingSteps = bitArray.readBits(8) + 1;\n        for (int j = 0; j < couplingSteps; j++) {\n          bitArray.skipBits(iLog(channels - 1)); // magnitude\n          bitArray.skipBits(iLog(channels - 1)); // angle\n        }\n      } /*else {\n          couplingSteps = 0;\n        }*/\n      if (bitArray.readBits(2) != 0x00) {\n        throw new ParserException(\"to reserved bits must be zero after mapping coupling steps\");\n      }\n      if (submaps > 1) {\n        for (int j = 0; j < channels; j++) {\n          bitArray.skipBits(4); // mappingMux\n        }\n      }\n      for (int j = 0; j < submaps; j++) {\n        bitArray.skipBits(8); // discard\n        bitArray.skipBits(8); // submapFloor\n        bitArray.skipBits(8); // submapResidue\n      }\n    }\n  }\n\n  private static void readResidues(VorbisBitArray bitArray) throws ParserException {\n    int residueCount = bitArray.readBits(6) + 1;\n    for (int i = 0; i < residueCount; i++) {\n      int residueType = bitArray.readBits(16);\n      if (residueType > 2) {\n        throw new ParserException(\"residueType greater than 2 is not decodable\");\n      } else {\n        bitArray.skipBits(24); // begin\n        bitArray.skipBits(24); // end\n        bitArray.skipBits(24); // partitionSize (add one)\n        int classifications = bitArray.readBits(6) + 1;\n        bitArray.skipBits(8); // classbook\n        int[] cascade = new int[classifications];\n        for (int j = 0; j < classifications; j++) {\n          int highBits = 0;\n          int lowBits = bitArray.readBits(3);\n          if (bitArray.readBit()) {\n            highBits = bitArray.readBits(5);\n          }\n          cascade[j] = highBits * 8 + lowBits;\n        }\n        for (int j = 0; j < classifications; j++) {\n          for (int k = 0; k < 8; k++) {\n            if ((cascade[j] & (0x01 << k)) != 0) {\n              bitArray.skipBits(8); // discard\n            }\n          }\n        }\n      }\n    }\n  }\n\n  private static void readFloors(VorbisBitArray bitArray) throws ParserException {\n    int floorCount = bitArray.readBits(6) + 1;\n    for (int i = 0; i < floorCount; i++) {\n      int floorType = bitArray.readBits(16);\n      switch (floorType) {\n        case 0:\n          bitArray.skipBits(8); //order\n          bitArray.skipBits(16); // rate\n          bitArray.skipBits(16); // barkMapSize\n          bitArray.skipBits(6); // amplitudeBits\n          bitArray.skipBits(8); // amplitudeOffset\n          int floorNumberOfBooks = bitArray.readBits(4) + 1;\n          for (int j = 0; j < floorNumberOfBooks; j++) {\n            bitArray.skipBits(8);\n          }\n          break;\n        case 1:\n          int partitions = bitArray.readBits(5);\n          int maximumClass = -1;\n          int[] partitionClassList = new int[partitions];\n          for (int j = 0; j < partitions; j++) {\n            partitionClassList[j] = bitArray.readBits(4);\n            if (partitionClassList[j] > maximumClass) {\n              maximumClass = partitionClassList[j];\n            }\n          }\n          int[] classDimensions = new int[maximumClass + 1];\n          for (int j = 0; j < classDimensions.length; j++) {\n            classDimensions[j] = bitArray.readBits(3) + 1;\n            int classSubclasses = bitArray.readBits(2);\n            if (classSubclasses > 0) {\n              bitArray.skipBits(8); // classMasterbooks\n            }\n            for (int k = 0; k < (1 << classSubclasses); k++) {\n              bitArray.skipBits(8); // subclassBook (subtract 1)\n            }\n          }\n          bitArray.skipBits(2); // multiplier (add one)\n          int rangeBits = bitArray.readBits(4);\n          int count = 0;\n          for (int j = 0, k = 0; j < partitions; j++) {\n            int idx = partitionClassList[j];\n            count += classDimensions[idx];\n            for (; k < count; k++) {\n              bitArray.skipBits(rangeBits); // floorValue\n            }\n          }\n          break;\n        default:\n          throw new ParserException(\"floor type greater than 1 not decodable: \" + floorType);\n      }\n    }\n  }\n\n  private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {\n    if (bitArray.readBits(24) != 0x564342) {\n      throw new ParserException(\"expected code book to start with [0x56, 0x43, 0x42] at \"\n          + bitArray.getPosition());\n    }\n    int dimensions = bitArray.readBits(16);\n    int entries = bitArray.readBits(24);\n    long[] lengthMap = new long[entries];\n\n    boolean isOrdered = bitArray.readBit();\n    if (!isOrdered) {\n      boolean isSparse = bitArray.readBit();\n      for (int i = 0; i < lengthMap.length; i++) {\n        if (isSparse) {\n          if (bitArray.readBit()) {\n            lengthMap[i] = (long) (bitArray.readBits(5) + 1);\n          } else { // entry unused\n            lengthMap[i] = 0;\n          }\n        } else { // not sparse\n          lengthMap[i] = (long) (bitArray.readBits(5) + 1);\n        }\n      }\n    } else {\n      int length = bitArray.readBits(5) + 1;\n      for (int i = 0; i < lengthMap.length;) {\n        int num = bitArray.readBits(iLog(entries - i));\n        for (int j = 0; j < num && i < lengthMap.length; i++, j++) {\n          lengthMap[i] = length;\n        }\n        length++;\n      }\n    }\n\n    int lookupType = bitArray.readBits(4);\n    if (lookupType > 2) {\n      throw new ParserException(\"lookup type greater than 2 not decodable: \" + lookupType);\n    } else if (lookupType == 1 || lookupType == 2) {\n      bitArray.skipBits(32); // minimumValue\n      bitArray.skipBits(32); // deltaValue\n      int valueBits = bitArray.readBits(4) + 1;\n      bitArray.skipBits(1); // sequenceP\n      long lookupValuesCount;\n      if (lookupType == 1) {\n        if (dimensions != 0) {\n          lookupValuesCount = mapType1QuantValues(entries, dimensions);\n        } else {\n          lookupValuesCount = 0;\n        }\n      } else {\n        lookupValuesCount = (long) entries * dimensions;\n      }\n      // discard (no decoding required yet)\n      bitArray.skipBits((int) (lookupValuesCount * valueBits));\n    }\n    return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);\n  }\n\n  /**\n   * @see <a href=\"http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c\">_book_maptype1_quantvals</a>\n   */\n  private static long mapType1QuantValues(long entries, long dimension) {\n    return (long) Math.floor(Math.pow(entries, 1.d / dimension));\n  }\n\n  private VorbisUtil() {\n    // Prevent instantiation.\n  }\n\n  public static final class CodeBook {\n\n    public final int dimensions;\n    public final int entries;\n    public final long[] lengthMap;\n    public final int lookupType;\n    public final boolean isOrdered;\n\n    public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,\n        boolean isOrdered) {\n      this.dimensions = dimensions;\n      this.entries = entries;\n      this.lengthMap = lengthMap;\n      this.lookupType = lookupType;\n      this.isOrdered = isOrdered;\n    }\n\n  }\n\n  public static final class CommentHeader {\n\n    public final String vendor;\n    public final String[] comments;\n    public final int length;\n\n    public CommentHeader(String vendor, String[] comments, int length) {\n      this.vendor = vendor;\n      this.comments = comments;\n      this.length = length;\n    }\n\n  }\n\n  public static final class VorbisIdHeader {\n\n    public final long version;\n    public final int channels;\n    public final long sampleRate;\n    public final int bitrateMax;\n    public final int bitrateNominal;\n    public final int bitrateMin;\n    public final int blockSize0;\n    public final int blockSize1;\n    public final boolean framingFlag;\n    public final byte[] data;\n\n    public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,\n        int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,\n        byte[] data) {\n      this.version = version;\n      this.channels = channels;\n      this.sampleRate = sampleRate;\n      this.bitrateMax = bitrateMax;\n      this.bitrateNominal = bitrateNominal;\n      this.bitrateMin = bitrateMin;\n      this.blockSize0 = blockSize0;\n      this.blockSize1 = blockSize1;\n      this.framingFlag = framingFlag;\n      this.data = data;\n    }\n\n    public int getApproximateBitrate() {\n      return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;\n    }\n\n  }\n\n  public static final class Mode {\n\n    public final boolean blockFlag;\n    public final int windowType;\n    public final int transformType;\n    public final int mapping;\n\n    public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {\n      this.blockFlag = blockFlag;\n      this.windowType = windowType;\n      this.transformType = transformType;\n      this.mapping = mapping;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.rawcc;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * Extracts data from the RawCC container format.\n */\npublic final class RawCcExtractor implements Extractor {\n\n  private static final int SCRATCH_SIZE = 9;\n  private static final int HEADER_SIZE = 8;\n  private static final int HEADER_ID = 0x52434301;\n  private static final int TIMESTAMP_SIZE_V0 = 4;\n  private static final int TIMESTAMP_SIZE_V1 = 8;\n\n  // Parser states.\n  private static final int STATE_READING_HEADER = 0;\n  private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1;\n  private static final int STATE_READING_SAMPLES = 2;\n\n  private final Format format;\n\n  private final ParsableByteArray dataScratch;\n\n  private TrackOutput trackOutput;\n\n  private int parserState;\n  private int version;\n  private long timestampUs;\n  private int remainingSampleCount;\n  private int sampleBytesWritten;\n\n  public RawCcExtractor(Format format) {\n    this.format = format;\n    dataScratch = new ParsableByteArray(SCRATCH_SIZE);\n    parserState = STATE_READING_HEADER;\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));\n    trackOutput = output.track(0, C.TRACK_TYPE_TEXT);\n    output.endTracks();\n    trackOutput.format(format);\n  }\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    dataScratch.reset();\n    input.peekFully(dataScratch.data, 0, HEADER_SIZE);\n    return dataScratch.readInt() == HEADER_ID;\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    while (true) {\n      switch (parserState) {\n        case STATE_READING_HEADER:\n          if (parseHeader(input)) {\n            parserState = STATE_READING_TIMESTAMP_AND_COUNT;\n          } else {\n            return RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_READING_TIMESTAMP_AND_COUNT:\n          if (parseTimestampAndSampleCount(input)) {\n            parserState = STATE_READING_SAMPLES;\n          } else {\n            parserState = STATE_READING_HEADER;\n            return RESULT_END_OF_INPUT;\n          }\n          break;\n        case STATE_READING_SAMPLES:\n          parseSamples(input);\n          parserState = STATE_READING_TIMESTAMP_AND_COUNT;\n          return RESULT_CONTINUE;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    parserState = STATE_READING_HEADER;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {\n    dataScratch.reset();\n    if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {\n      if (dataScratch.readInt() != HEADER_ID) {\n        throw new IOException(\"Input not RawCC\");\n      }\n      version = dataScratch.readUnsignedByte();\n      // no versions use the flag fields yet\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,\n      InterruptedException {\n    dataScratch.reset();\n    if (version == 0) {\n      if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) {\n        return false;\n      }\n      // version 0 timestamps are 45kHz, so we need to convert them into us\n      timestampUs = dataScratch.readUnsignedInt() * 1000 / 45;\n    } else if (version == 1) {\n      if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) {\n        return false;\n      }\n      timestampUs = dataScratch.readLong();\n    } else {\n      throw new ParserException(\"Unsupported version number: \" + version);\n    }\n\n    remainingSampleCount = dataScratch.readUnsignedByte();\n    sampleBytesWritten = 0;\n    return true;\n  }\n\n  private void parseSamples(ExtractorInput input) throws IOException, InterruptedException {\n    for (; remainingSampleCount > 0; remainingSampleCount--) {\n      dataScratch.reset();\n      input.readFully(dataScratch.data, 0, 3);\n\n      trackOutput.sampleData(dataScratch, 3);\n      sampleBytesWritten += 3;\n    }\n\n    if (sampleBytesWritten > 0) {\n      trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.audio.Ac3Util;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * Extracts data from (E-)AC-3 bitstreams.\n */\npublic final class Ac3Extractor implements Extractor {\n\n  /** Factory for {@link Ac3Extractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()};\n\n  /**\n   * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving\n   * up.\n   */\n  private static final int MAX_SNIFF_BYTES = 8 * 1024;\n  private static final int AC3_SYNC_WORD = 0x0B77;\n  private static final int MAX_SYNC_FRAME_SIZE = 2786;\n\n  private final Ac3Reader reader;\n  private final ParsableByteArray sampleData;\n\n  private boolean startedPacket;\n\n  /** Creates a new extractor for AC-3 bitstreams. */\n  public Ac3Extractor() {\n    reader = new Ac3Reader();\n    sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    // Skip any ID3 headers.\n    ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH);\n    int startPosition = 0;\n    while (true) {\n      input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);\n      scratch.setPosition(0);\n      if (scratch.readUnsignedInt24() != ID3_TAG) {\n        break;\n      }\n      scratch.skipBytes(3); // version, flags\n      int length = scratch.readSynchSafeInt();\n      startPosition += 10 + length;\n      input.advancePeekPosition(length);\n    }\n    input.resetPeekPosition();\n    input.advancePeekPosition(startPosition);\n\n    int headerPosition = startPosition;\n    int validFramesCount = 0;\n    while (true) {\n      input.peekFully(scratch.data, 0, 6);\n      scratch.setPosition(0);\n      int syncBytes = scratch.readUnsignedShort();\n      if (syncBytes != AC3_SYNC_WORD) {\n        validFramesCount = 0;\n        input.resetPeekPosition();\n        if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {\n          return false;\n        }\n        input.advancePeekPosition(headerPosition);\n      } else {\n        if (++validFramesCount >= 4) {\n          return true;\n        }\n        int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data);\n        if (frameSize == C.LENGTH_UNSET) {\n          return false;\n        }\n        input.advancePeekPosition(frameSize - 6);\n      }\n    }\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    reader.createTracks(output, new TrackIdGenerator(0, 1));\n    output.endTracks();\n    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    startedPacket = false;\n    reader.seek();\n  }\n\n  @Override\n  public void release() {\n    // Do nothing.\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,\n      InterruptedException {\n    int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE);\n    if (bytesRead == C.RESULT_END_OF_INPUT) {\n      return RESULT_END_OF_INPUT;\n    }\n\n    // Feed whatever data we have to the reader, regardless of whether the read finished or not.\n    sampleData.setPosition(0);\n    sampleData.setLimit(bytesRead);\n\n    if (!startedPacket) {\n      // Pass data to the reader as though it's contained within a single infinitely long packet.\n      reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR);\n      startedPacket = true;\n    }\n    // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes\n    // unnecessary to copy the data through packetBuffer.\n    reader.consume(sampleData);\n    return RESULT_CONTINUE;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.Ac3Util;\nimport com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Parses a continuous (E-)AC-3 byte stream and extracts individual samples.\n */\npublic final class Ac3Reader implements ElementaryStreamReader {\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE})\n  private @interface State {}\n\n  private static final int STATE_FINDING_SYNC = 0;\n  private static final int STATE_READING_HEADER = 1;\n  private static final int STATE_READING_SAMPLE = 2;\n\n  private static final int HEADER_SIZE = 128;\n\n  private final ParsableBitArray headerScratchBits;\n  private final ParsableByteArray headerScratchBytes;\n  private final String language;\n\n  private String trackFormatId;\n  private TrackOutput output;\n\n  @State private int state;\n  private int bytesRead;\n\n  // Used to find the header.\n  private boolean lastByteWas0B;\n\n  // Used when parsing the header.\n  private long sampleDurationUs;\n  private Format format;\n  private int sampleSize;\n\n  // Used when reading the samples.\n  private long timeUs;\n\n  /**\n   * Constructs a new reader for (E-)AC-3 elementary streams.\n   */\n  public Ac3Reader() {\n    this(null);\n  }\n\n  /**\n   * Constructs a new reader for (E-)AC-3 elementary streams.\n   *\n   * @param language Track language.\n   */\n  public Ac3Reader(String language) {\n    headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);\n    headerScratchBytes = new ParsableByteArray(headerScratchBits.data);\n    state = STATE_FINDING_SYNC;\n    this.language = language;\n  }\n\n  @Override\n  public void seek() {\n    state = STATE_FINDING_SYNC;\n    bytesRead = 0;\n    lastByteWas0B = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {\n    generator.generateNewId();\n    trackFormatId = generator.getFormatId();\n    output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_SYNC:\n          if (skipToNextSync(data)) {\n            state = STATE_READING_HEADER;\n            headerScratchBytes.data[0] = 0x0B;\n            headerScratchBytes.data[1] = 0x77;\n            bytesRead = 2;\n          }\n          break;\n        case STATE_READING_HEADER:\n          if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {\n            parseHeader();\n            headerScratchBytes.setPosition(0);\n            output.sampleData(headerScratchBytes, HEADER_SIZE);\n            state = STATE_READING_SAMPLE;\n          }\n          break;\n        case STATE_READING_SAMPLE:\n          int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);\n          output.sampleData(data, bytesToRead);\n          bytesRead += bytesToRead;\n          if (bytesRead == sampleSize) {\n            output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n            timeUs += sampleDurationUs;\n            state = STATE_FINDING_SYNC;\n          }\n          break;\n        default:\n          break;\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed\n   * that the data should be written into {@code target} starting from an offset of zero.\n   *\n   * @param source The source from which to read.\n   * @param target The target into which data is to be read.\n   * @param targetLength The target length of the read.\n   * @return Whether the target length was reached.\n   */\n  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {\n    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);\n    source.readBytes(target, bytesRead, bytesToRead);\n    bytesRead += bytesToRead;\n    return bytesRead == targetLength;\n  }\n\n  /**\n   * Locates the next syncword, advancing the position to the byte that immediately follows it. If a\n   * syncword was not located, the position is advanced to the limit.\n   *\n   * @param pesBuffer The buffer whose position should be advanced.\n   * @return Whether a syncword position was found.\n   */\n  private boolean skipToNextSync(ParsableByteArray pesBuffer) {\n    while (pesBuffer.bytesLeft() > 0) {\n      if (!lastByteWas0B) {\n        lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B;\n        continue;\n      }\n      int secondByte = pesBuffer.readUnsignedByte();\n      if (secondByte == 0x77) {\n        lastByteWas0B = false;\n        return true;\n      } else {\n        lastByteWas0B = secondByte == 0x0B;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Parses the sample header.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  private void parseHeader() {\n    headerScratchBits.setPosition(0);\n    SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);\n    if (format == null || frameInfo.channelCount != format.channelCount\n        || frameInfo.sampleRate != format.sampleRate\n        || frameInfo.mimeType != format.sampleMimeType) {\n      format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null,\n          Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null,\n          null, 0, language);\n      output.format(format);\n    }\n    sampleSize = frameInfo.frameSize;\n    // In this class a sample is an access unit (syncframe in AC-3), but Format#sampleRate\n    // specifies the number of PCM audio samples per second.\n    sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD;\nimport static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD;\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.audio.Ac4Util;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/** Extracts data from AC-4 bitstreams. */\npublic final class Ac4Extractor implements Extractor {\n\n  /** Factory for {@link Ac4Extractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()};\n\n  /**\n   * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving\n   * up.\n   */\n  private static final int MAX_SNIFF_BYTES = 8 * 1024;\n\n  /**\n   * The size of the reading buffer, in bytes. This value is determined based on the maximum frame\n   * size used in broadcast applications.\n   */\n  private static final int READ_BUFFER_SIZE = 16384;\n\n  /** The size of the frame header, in bytes. */\n  private static final int FRAME_HEADER_SIZE = 7;\n\n  private final Ac4Reader reader;\n  private final ParsableByteArray sampleData;\n\n  private boolean startedPacket;\n\n  /** Creates a new extractor for AC-4 bitstreams. */\n  public Ac4Extractor() {\n    reader = new Ac4Reader();\n    sampleData = new ParsableByteArray(READ_BUFFER_SIZE);\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    // Skip any ID3 headers.\n    ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH);\n    int startPosition = 0;\n    while (true) {\n      input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);\n      scratch.setPosition(0);\n      if (scratch.readUnsignedInt24() != ID3_TAG) {\n        break;\n      }\n      scratch.skipBytes(3); // version, flags\n      int length = scratch.readSynchSafeInt();\n      startPosition += 10 + length;\n      input.advancePeekPosition(length);\n    }\n    input.resetPeekPosition();\n    input.advancePeekPosition(startPosition);\n\n    int headerPosition = startPosition;\n    int validFramesCount = 0;\n    while (true) {\n      input.peekFully(scratch.data, /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE);\n      scratch.setPosition(0);\n      int syncBytes = scratch.readUnsignedShort();\n      if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) {\n        validFramesCount = 0;\n        input.resetPeekPosition();\n        if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {\n          return false;\n        }\n        input.advancePeekPosition(headerPosition);\n      } else {\n        if (++validFramesCount >= 4) {\n          return true;\n        }\n        int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.data, syncBytes);\n        if (frameSize == C.LENGTH_UNSET) {\n          return false;\n        }\n        input.advancePeekPosition(frameSize - FRAME_HEADER_SIZE);\n      }\n    }\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    reader.createTracks(\n        output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1));\n    output.endTracks();\n    output.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    startedPacket = false;\n    reader.seek();\n  }\n\n  @Override\n  public void release() {\n    // Do nothing.\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    int bytesRead = input.read(sampleData.data, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE);\n    if (bytesRead == C.RESULT_END_OF_INPUT) {\n      return RESULT_END_OF_INPUT;\n    }\n\n    // Feed whatever data we have to the reader, regardless of whether the read finished or not.\n    sampleData.setPosition(0);\n    sampleData.setLimit(bytesRead);\n\n    if (!startedPacket) {\n      // Pass data to the reader as though it's contained within a single infinitely long packet.\n      reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR);\n      startedPacket = true;\n    }\n    // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes\n    // unnecessary to copy the data through packetBuffer.\n    reader.consume(sampleData);\n    return RESULT_CONTINUE;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.Ac4Util;\nimport com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Parses a continuous AC-4 byte stream and extracts individual samples. */\npublic final class Ac4Reader implements ElementaryStreamReader {\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE})\n  private @interface State {}\n\n  private static final int STATE_FINDING_SYNC = 0;\n  private static final int STATE_READING_HEADER = 1;\n  private static final int STATE_READING_SAMPLE = 2;\n\n  private final ParsableBitArray headerScratchBits;\n  private final ParsableByteArray headerScratchBytes;\n  private final String language;\n\n  private String trackFormatId;\n  private TrackOutput output;\n\n  @State private int state;\n  private int bytesRead;\n\n  // Used to find the header.\n  private boolean lastByteWasAC;\n  private boolean hasCRC;\n\n  // Used when parsing the header.\n  private long sampleDurationUs;\n  private Format format;\n  private int sampleSize;\n\n  // Used when reading the samples.\n  private long timeUs;\n\n  /** Constructs a new reader for AC-4 elementary streams. */\n  public Ac4Reader() {\n    this(null);\n  }\n\n  /**\n   * Constructs a new reader for AC-4 elementary streams.\n   *\n   * @param language Track language.\n   */\n  public Ac4Reader(String language) {\n    headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]);\n    headerScratchBytes = new ParsableByteArray(headerScratchBits.data);\n    state = STATE_FINDING_SYNC;\n    bytesRead = 0;\n    lastByteWasAC = false;\n    hasCRC = false;\n    this.language = language;\n  }\n\n  @Override\n  public void seek() {\n    state = STATE_FINDING_SYNC;\n    bytesRead = 0;\n    lastByteWasAC = false;\n    hasCRC = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {\n    generator.generateNewId();\n    trackFormatId = generator.getFormatId();\n    output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_SYNC:\n          if (skipToNextSync(data)) {\n            state = STATE_READING_HEADER;\n            headerScratchBytes.data[0] = (byte) 0xAC;\n            headerScratchBytes.data[1] = (byte) (hasCRC ? 0x41 : 0x40);\n            bytesRead = 2;\n          }\n          break;\n        case STATE_READING_HEADER:\n          if (continueRead(data, headerScratchBytes.data, Ac4Util.HEADER_SIZE_FOR_PARSER)) {\n            parseHeader();\n            headerScratchBytes.setPosition(0);\n            output.sampleData(headerScratchBytes, Ac4Util.HEADER_SIZE_FOR_PARSER);\n            state = STATE_READING_SAMPLE;\n          }\n          break;\n        case STATE_READING_SAMPLE:\n          int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);\n          output.sampleData(data, bytesToRead);\n          bytesRead += bytesToRead;\n          if (bytesRead == sampleSize) {\n            output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n            timeUs += sampleDurationUs;\n            state = STATE_FINDING_SYNC;\n          }\n          break;\n        default:\n          break;\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed\n   * that the data should be written into {@code target} starting from an offset of zero.\n   *\n   * @param source The source from which to read.\n   * @param target The target into which data is to be read.\n   * @param targetLength The target length of the read.\n   * @return Whether the target length was reached.\n   */\n  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {\n    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);\n    source.readBytes(target, bytesRead, bytesToRead);\n    bytesRead += bytesToRead;\n    return bytesRead == targetLength;\n  }\n\n  /**\n   * Locates the next syncword, advancing the position to the byte that immediately follows it. If a\n   * syncword was not located, the position is advanced to the limit.\n   *\n   * @param pesBuffer The buffer whose position should be advanced.\n   * @return Whether a syncword position was found.\n   */\n  private boolean skipToNextSync(ParsableByteArray pesBuffer) {\n    while (pesBuffer.bytesLeft() > 0) {\n      if (!lastByteWasAC) {\n        lastByteWasAC = (pesBuffer.readUnsignedByte() == 0xAC);\n        continue;\n      }\n      int secondByte = pesBuffer.readUnsignedByte();\n      lastByteWasAC = secondByte == 0xAC;\n      if (secondByte == 0x40 || secondByte == 0x41) {\n        hasCRC = secondByte == 0x41;\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /** Parses the sample header. */\n  @SuppressWarnings(\"ReferenceEquality\")\n  private void parseHeader() {\n    headerScratchBits.setPosition(0);\n    SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits);\n    if (format == null\n        || frameInfo.channelCount != format.channelCount\n        || frameInfo.sampleRate != format.sampleRate\n        || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) {\n      format =\n          Format.createAudioSampleFormat(\n              trackFormatId,\n              MimeTypes.AUDIO_AC4,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              /* maxInputSize= */ Format.NO_VALUE,\n              frameInfo.channelCount,\n              frameInfo.sampleRate,\n              /* initializationData= */ null,\n              /* drmInitData= */ null,\n              /* selectionFlags= */ 0,\n              language);\n      output.format(format);\n    }\n    sampleSize = frameInfo.frameSize;\n    // In this class a sample is an AC-4 sync frame, but Format#sampleRate specifies the number of\n    // PCM audio samples per second.\n    sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Extracts data from AAC bit streams with ADTS framing.\n */\npublic final class AdtsExtractor implements Extractor {\n\n  /** Factory for {@link AdtsExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag value is {@link\n   * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})\n  public @interface Flags {}\n  /**\n   * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would\n   * otherwise not be possible.\n   *\n   * <p>Note that this approach may result in approximated stream duration and seek position that\n   * are not precise, especially when the stream bitrate varies a lot.\n   */\n  public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;\n\n  private static final int MAX_PACKET_SIZE = 2 * 1024;\n  /**\n   * The maximum number of bytes to search when sniffing, excluding the header, before giving up.\n   * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes.\n   */\n  private static final int MAX_SNIFF_BYTES = 8 * 1024;\n  /**\n   * The maximum number of frames to use when calculating the average frame size for constant\n   * bitrate seeking.\n   */\n  private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000;\n\n  private final @Flags int flags;\n\n  private final AdtsReader reader;\n  private final ParsableByteArray packetBuffer;\n  private final ParsableByteArray scratch;\n  private final ParsableBitArray scratchBits;\n\n  @Nullable private ExtractorOutput extractorOutput;\n\n  private long firstSampleTimestampUs;\n  private long firstFramePosition;\n  private int averageFrameSize;\n  private boolean hasCalculatedAverageFrameSize;\n  private boolean startedPacket;\n  private boolean hasOutputSeekMap;\n\n  /** Creates a new extractor for ADTS bitstreams. */\n  public AdtsExtractor() {\n    this(/* flags= */ 0);\n  }\n\n  /**\n   * Creates a new extractor for ADTS bitstreams.\n   *\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public AdtsExtractor(@Flags int flags) {\n    this.flags = flags;\n    reader = new AdtsReader(true);\n    packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);\n    averageFrameSize = C.LENGTH_UNSET;\n    firstFramePosition = C.POSITION_UNSET;\n    // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values.\n    scratch = new ParsableByteArray(ID3_HEADER_LENGTH);\n    scratchBits = new ParsableBitArray(scratch.data);\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    // Skip any ID3 headers.\n    int startPosition = peekId3Header(input);\n\n    // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.\n    int headerPosition = startPosition;\n    int totalValidFramesSize = 0;\n    int validFramesCount = 0;\n    while (true) {\n      input.peekFully(scratch.data, 0, 2);\n      scratch.setPosition(0);\n      int syncBytes = scratch.readUnsignedShort();\n      if (!AdtsReader.isAdtsSyncWord(syncBytes)) {\n        validFramesCount = 0;\n        totalValidFramesSize = 0;\n        input.resetPeekPosition();\n        if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {\n          return false;\n        }\n        input.advancePeekPosition(headerPosition);\n      } else {\n        if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) {\n          return true;\n        }\n\n        // Skip the frame.\n        input.peekFully(scratch.data, 0, 4);\n        scratchBits.setPosition(14);\n        int frameSize = scratchBits.readBits(13);\n        // Either the stream is malformed OR we're not parsing an ADTS stream.\n        if (frameSize <= 6) {\n          return false;\n        }\n        input.advancePeekPosition(frameSize - 6);\n        totalValidFramesSize += frameSize;\n      }\n    }\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.extractorOutput = output;\n    reader.createTracks(output, new TrackIdGenerator(0, 1));\n    output.endTracks();\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    startedPacket = false;\n    reader.seek();\n    firstSampleTimestampUs = timeUs;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    boolean canUseConstantBitrateSeeking =\n        (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET;\n    if (canUseConstantBitrateSeeking) {\n      calculateAverageFrameSize(input);\n    }\n\n    int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);\n    boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;\n    maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream);\n    if (readEndOfStream) {\n      return RESULT_END_OF_INPUT;\n    }\n\n    // Feed whatever data we have to the reader, regardless of whether the read finished or not.\n    packetBuffer.setPosition(0);\n    packetBuffer.setLimit(bytesRead);\n\n    if (!startedPacket) {\n      // Pass data to the reader as though it's contained within a single infinitely long packet.\n      reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);\n      startedPacket = true;\n    }\n    // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes\n    // unnecessary to copy the data through packetBuffer.\n    reader.consume(packetBuffer);\n    return RESULT_CONTINUE;\n  }\n\n  private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException {\n    int firstFramePosition = 0;\n    while (true) {\n      input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);\n      scratch.setPosition(0);\n      if (scratch.readUnsignedInt24() != ID3_TAG) {\n        break;\n      }\n      scratch.skipBytes(3);\n      int length = scratch.readSynchSafeInt();\n      firstFramePosition += ID3_HEADER_LENGTH + length;\n      input.advancePeekPosition(length);\n    }\n    input.resetPeekPosition();\n    input.advancePeekPosition(firstFramePosition);\n    if (this.firstFramePosition == C.POSITION_UNSET) {\n      this.firstFramePosition = firstFramePosition;\n    }\n    return firstFramePosition;\n  }\n\n  private void maybeOutputSeekMap(\n      long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) {\n    if (hasOutputSeekMap) {\n      return;\n    }\n    boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0;\n    if (useConstantBitrateSeeking\n        && reader.getSampleDurationUs() == C.TIME_UNSET\n        && !readEndOfStream) {\n      // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached,\n      // before creating seek map.\n      return;\n    }\n\n    ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput);\n    if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) {\n      extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength));\n    } else {\n      extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));\n    }\n    hasOutputSeekMap = true;\n  }\n\n  private void calculateAverageFrameSize(ExtractorInput input)\n      throws IOException, InterruptedException {\n    if (hasCalculatedAverageFrameSize) {\n      return;\n    }\n    averageFrameSize = C.LENGTH_UNSET;\n    input.resetPeekPosition();\n    if (input.getPosition() == 0) {\n      // Skip any ID3 headers.\n      peekId3Header(input);\n    }\n\n    int numValidFrames = 0;\n    long totalValidFramesSize = 0;\n    try {\n      while (input.peekFully(\n          scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) {\n        scratch.setPosition(0);\n        int syncBytes = scratch.readUnsignedShort();\n        if (!AdtsReader.isAdtsSyncWord(syncBytes)) {\n          // Invalid sync byte pattern.\n          // Constant bit-rate seeking will probably fail for this stream.\n          numValidFrames = 0;\n          break;\n        } else {\n          // Read the frame size.\n          if (!input.peekFully(\n              scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) {\n            break;\n          }\n          scratchBits.setPosition(14);\n          int currentFrameSize = scratchBits.readBits(13);\n          // Either the stream is malformed OR we're not parsing an ADTS stream.\n          if (currentFrameSize <= 6) {\n            hasCalculatedAverageFrameSize = true;\n            throw new ParserException(\"Malformed ADTS stream\");\n          }\n          totalValidFramesSize += currentFrameSize;\n          if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) {\n            break;\n          }\n          if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) {\n            break;\n          }\n        }\n      }\n    } catch (EOFException e) {\n      // We reached the end of the input during a peekFully() or advancePeekPosition() operation.\n      // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally\n      // ExtractorInput would allow these operations to encounter end-of-input without throwing an\n      // exception [internal: b/145586657].\n    }\n    input.resetPeekPosition();\n    if (numValidFrames > 0) {\n      averageFrameSize = (int) (totalValidFramesSize / numValidFrames);\n    } else {\n      averageFrameSize = C.LENGTH_UNSET;\n    }\n    hasCalculatedAverageFrameSize = true;\n  }\n\n  private SeekMap getConstantBitrateSeekMap(long inputLength) {\n    int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs());\n    return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize);\n  }\n\n  /**\n   * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.\n   *\n   * @param frameSize The size of each frame in the stream.\n   * @param durationUsPerFrame The duration of the given frame in microseconds.\n   * @return The stream bitrate.\n   */\n  private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {\n    return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.DummyTrackOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Arrays;\nimport java.util.Collections;\n\n/**\n * Parses a continuous ADTS byte stream and extracts individual frames.\n */\npublic final class AdtsReader implements ElementaryStreamReader {\n\n  private static final String TAG = \"AdtsReader\";\n\n  private static final int STATE_FINDING_SAMPLE = 0;\n  private static final int STATE_CHECKING_ADTS_HEADER = 1;\n  private static final int STATE_READING_ID3_HEADER = 2;\n  private static final int STATE_READING_ADTS_HEADER = 3;\n  private static final int STATE_READING_SAMPLE = 4;\n\n  private static final int HEADER_SIZE = 5;\n  private static final int CRC_SIZE = 2;\n\n  // Match states used while looking for the next sample\n  private static final int MATCH_STATE_VALUE_SHIFT = 8;\n  private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT;\n  private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT;\n  private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT;\n  private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT;\n\n  private static final int ID3_HEADER_SIZE = 10;\n  private static final int ID3_SIZE_OFFSET = 6;\n  private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};\n  private static final int VERSION_UNSET = -1;\n\n  private final boolean exposeId3;\n  private final ParsableBitArray adtsScratch;\n  private final ParsableByteArray id3HeaderBuffer;\n  private final String language;\n\n  private String formatId;\n  private TrackOutput output;\n  private TrackOutput id3Output;\n\n  private int state;\n  private int bytesRead;\n\n  private int matchState;\n\n  private boolean hasCrc;\n  private boolean foundFirstFrame;\n\n  // Used to verifies sync words\n  private int firstFrameVersion;\n  private int firstFrameSampleRateIndex;\n\n  private int currentFrameVersion;\n\n  // Used when parsing the header.\n  private boolean hasOutputFormat;\n  private long sampleDurationUs;\n  private int sampleSize;\n\n  // Used when reading the samples.\n  private long timeUs;\n\n  private TrackOutput currentOutput;\n  private long currentSampleDuration;\n\n  /**\n   * @param exposeId3 True if the reader should expose ID3 information.\n   */\n  public AdtsReader(boolean exposeId3) {\n    this(exposeId3, null);\n  }\n\n  /**\n   * @param exposeId3 True if the reader should expose ID3 information.\n   * @param language Track language.\n   */\n  public AdtsReader(boolean exposeId3, String language) {\n    adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);\n    id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));\n    setFindingSampleState();\n    firstFrameVersion = VERSION_UNSET;\n    firstFrameSampleRateIndex = C.INDEX_UNSET;\n    sampleDurationUs = C.TIME_UNSET;\n    this.exposeId3 = exposeId3;\n    this.language = language;\n  }\n\n  /** Returns whether an integer matches an ADTS SYNC word. */\n  public static boolean isAdtsSyncWord(int candidateSyncWord) {\n    return (candidateSyncWord & 0xFFF6) == 0xFFF0;\n  }\n\n  @Override\n  public void seek() {\n    resetSync();\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);\n    if (exposeId3) {\n      idGenerator.generateNewId();\n      id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);\n      id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(),\n          MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null));\n    } else {\n      id3Output = new DummyTrackOutput();\n    }\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) throws ParserException {\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_SAMPLE:\n          findNextSample(data);\n          break;\n        case STATE_READING_ID3_HEADER:\n          if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {\n            parseId3Header();\n          }\n          break;\n        case STATE_CHECKING_ADTS_HEADER:\n          checkAdtsHeader(data);\n          break;\n        case STATE_READING_ADTS_HEADER:\n          int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;\n          if (continueRead(data, adtsScratch.data, targetLength)) {\n            parseAdtsHeader();\n          }\n          break;\n        case STATE_READING_SAMPLE:\n          readSample(data);\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration\n   * is not available.\n   */\n  public long getSampleDurationUs() {\n    return sampleDurationUs;\n  }\n\n  private void resetSync() {\n    foundFirstFrame = false;\n    setFindingSampleState();\n  }\n\n  /**\n   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed\n   * that the data should be written into {@code target} starting from an offset of zero.\n   *\n   * @param source The source from which to read.\n   * @param target The target into which data is to be read.\n   * @param targetLength The target length of the read.\n   * @return Whether the target length was reached.\n   */\n  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {\n    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);\n    source.readBytes(target, bytesRead, bytesToRead);\n    bytesRead += bytesToRead;\n    return bytesRead == targetLength;\n  }\n\n  /**\n   * Sets the state to STATE_FINDING_SAMPLE.\n   */\n  private void setFindingSampleState() {\n    state = STATE_FINDING_SAMPLE;\n    bytesRead = 0;\n    matchState = MATCH_STATE_START;\n  }\n\n  /**\n   * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for\n   * {@link #parseId3Header()}.\n   */\n  private void setReadingId3HeaderState() {\n    state = STATE_READING_ID3_HEADER;\n    bytesRead = ID3_IDENTIFIER.length;\n    sampleSize = 0;\n    id3HeaderBuffer.setPosition(0);\n  }\n\n  /**\n   * Sets the state to STATE_READING_SAMPLE.\n   *\n   * @param outputToUse TrackOutput object to write the sample to\n   * @param currentSampleDuration Duration of the sample to be read\n   * @param priorReadBytes Size of prior read bytes\n   * @param sampleSize Size of the sample\n   */\n  private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration,\n      int priorReadBytes, int sampleSize) {\n    state = STATE_READING_SAMPLE;\n    bytesRead = priorReadBytes;\n    this.currentOutput = outputToUse;\n    this.currentSampleDuration = currentSampleDuration;\n    this.sampleSize = sampleSize;\n  }\n\n  /**\n   * Sets the state to STATE_READING_ADTS_HEADER.\n   */\n  private void setReadingAdtsHeaderState() {\n    state = STATE_READING_ADTS_HEADER;\n    bytesRead = 0;\n  }\n\n  /** Sets the state to STATE_CHECKING_ADTS_HEADER. */\n  private void setCheckingAdtsHeaderState() {\n    state = STATE_CHECKING_ADTS_HEADER;\n    bytesRead = 0;\n  }\n\n  /**\n   * Locates the next sample start, advancing the position to the byte that immediately follows\n   * identifier. If a sample was not located, the position is advanced to the limit.\n   *\n   * @param pesBuffer The buffer whose position should be advanced.\n   */\n  private void findNextSample(ParsableByteArray pesBuffer) {\n    byte[] adtsData = pesBuffer.data;\n    int position = pesBuffer.getPosition();\n    int endOffset = pesBuffer.limit();\n    while (position < endOffset) {\n      int data = adtsData[position++] & 0xFF;\n      if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) {\n        if (foundFirstFrame\n            || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) {\n          currentFrameVersion = (data & 0x8) >> 3;\n          hasCrc = (data & 0x1) == 0;\n          if (!foundFirstFrame) {\n            setCheckingAdtsHeaderState();\n          } else {\n            setReadingAdtsHeaderState();\n          }\n          pesBuffer.setPosition(position);\n          return;\n        }\n      }\n\n      switch (matchState | data) {\n        case MATCH_STATE_START | 0xFF:\n          matchState = MATCH_STATE_FF;\n          break;\n        case MATCH_STATE_START | 'I':\n          matchState = MATCH_STATE_I;\n          break;\n        case MATCH_STATE_I | 'D':\n          matchState = MATCH_STATE_ID;\n          break;\n        case MATCH_STATE_ID | '3':\n          setReadingId3HeaderState();\n          pesBuffer.setPosition(position);\n          return;\n        default:\n          if (matchState != MATCH_STATE_START) {\n            // If matching fails in a later state, revert to MATCH_STATE_START and\n            // check this byte again\n            matchState = MATCH_STATE_START;\n            position--;\n          }\n          break;\n      }\n    }\n    pesBuffer.setPosition(position);\n  }\n\n  /**\n   * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid,\n   * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link\n   * #STATE_FINDING_SAMPLE}.\n   */\n  private void checkAdtsHeader(ParsableByteArray buffer) {\n    if (buffer.bytesLeft() == 0) {\n      // Not enough data to check yet, defer this check.\n      return;\n    }\n    // Peek the next byte of buffer into scratch array.\n    adtsScratch.data[0] = buffer.data[buffer.getPosition()];\n\n    adtsScratch.setPosition(2);\n    int currentFrameSampleRateIndex = adtsScratch.readBits(4);\n    if (firstFrameSampleRateIndex != C.INDEX_UNSET\n        && currentFrameSampleRateIndex != firstFrameSampleRateIndex) {\n      // Invalid header.\n      resetSync();\n      return;\n    }\n\n    if (!foundFirstFrame) {\n      foundFirstFrame = true;\n      firstFrameVersion = currentFrameVersion;\n      firstFrameSampleRateIndex = currentFrameSampleRateIndex;\n    }\n    setReadingAdtsHeaderState();\n  }\n\n  /**\n   * Returns whether the given syncPositionCandidate is a real SYNC word.\n   *\n   * <p>SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is\n   * really a SYNC word. This includes:\n   *\n   * <ul>\n   *   <li>Checking if MPEG version of this frame matches the first detected version.\n   *   <li>Checking if the sample rate index of this frame matches the first detected sample rate\n   *       index.\n   *   <li>Checking if the bytes immediately after the current package also match a SYNC-word.\n   * </ul>\n   *\n   * If the buffer runs out of data for any check, optimistically skip that check, because\n   * AdtsReader consumes each buffer as a whole. We will still run a header validity check later.\n   */\n  private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) {\n    // The SYNC word contains 2 bytes, and the first byte may be in the previously consumed buffer.\n    // Hence the second byte of the SYNC word may be byte 0 of this buffer, and\n    // syncPositionCandidate (which indicates position of the first byte of the SYNC word) may be\n    // -1.\n    // Since the first byte of the SYNC word is always FF, which does not contain any informational\n    // bits, we set the byte position to be the second byte in the SYNC word to ensure it's always\n    // within this buffer.\n    pesBuffer.setPosition(syncPositionCandidate + 1);\n    if (!tryRead(pesBuffer, adtsScratch.data, 1)) {\n      return false;\n    }\n\n    adtsScratch.setPosition(4);\n    int currentFrameVersion = adtsScratch.readBits(1);\n    if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) {\n      return false;\n    }\n\n    if (firstFrameSampleRateIndex != C.INDEX_UNSET) {\n      if (!tryRead(pesBuffer, adtsScratch.data, 1)) {\n        return true;\n      }\n      adtsScratch.setPosition(2);\n      int currentFrameSampleRateIndex = adtsScratch.readBits(4);\n      if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) {\n        return false;\n      }\n      pesBuffer.setPosition(syncPositionCandidate + 2);\n    }\n\n    // Optionally check the byte after this frame matches SYNC word.\n\n    if (!tryRead(pesBuffer, adtsScratch.data, 4)) {\n      return true;\n    }\n    adtsScratch.setPosition(14);\n    int frameSize = adtsScratch.readBits(13);\n    if (frameSize <= 6) {\n      // Not a frame.\n      return false;\n    }\n    int nextSyncPosition = syncPositionCandidate + frameSize;\n    if (nextSyncPosition + 1 >= pesBuffer.limit()) {\n      return true;\n    }\n    return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1])\n        && (firstFrameVersion == VERSION_UNSET\n            || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion));\n  }\n\n  private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) {\n    int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF);\n    return isAdtsSyncWord(syncWord);\n  }\n\n  /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */\n  private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) {\n    if (source.bytesLeft() < targetLength) {\n      return false;\n    }\n    source.readBytes(target, /* offset= */ 0, targetLength);\n    return true;\n  }\n\n  /**\n   * Parses the Id3 header.\n   */\n  private void parseId3Header() {\n    id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);\n    id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);\n    setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE,\n        id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);\n  }\n\n  /**\n   * Parses the sample header.\n   */\n  private void parseAdtsHeader() throws ParserException {\n    adtsScratch.setPosition(0);\n\n    if (!hasOutputFormat) {\n      int audioObjectType = adtsScratch.readBits(2) + 1;\n      if (audioObjectType != 2) {\n        // The stream indicates AAC-Main (1), AAC-SSR (3) or AAC-LTP (4). When the stream indicates\n        // AAC-Main it's more likely that the stream contains HE-AAC (5), which cannot be\n        // represented correctly in the 2 bit audio_object_type field in the ADTS header. In\n        // practice when the stream indicates AAC-SSR or AAC-LTP it more commonly contains AAC-LC or\n        // HE-AAC. Since most Android devices don't support AAC-Main, AAC-SSR or AAC-LTP, and since\n        // indicating AAC-LC works for HE-AAC streams, we pretend that we're dealing with AAC-LC and\n        // hope for the best. In practice this often works.\n        // See: https://github.com/google/ExoPlayer/issues/774\n        // See: https://github.com/google/ExoPlayer/issues/1383\n        Log.w(TAG, \"Detected audio object type: \" + audioObjectType + \", but assuming AAC LC.\");\n        audioObjectType = 2;\n      }\n\n      adtsScratch.skipBits(5);\n      int channelConfig = adtsScratch.readBits(3);\n\n      byte[] audioSpecificConfig =\n          CodecSpecificDataUtil.buildAacAudioSpecificConfig(\n              audioObjectType, firstFrameSampleRateIndex, channelConfig);\n      Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(\n          audioSpecificConfig);\n\n      Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,\n          Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,\n          Collections.singletonList(audioSpecificConfig), null, 0, language);\n      // In this class a sample is an access unit, but the MediaFormat sample rate specifies the\n      // number of PCM audio samples per second.\n      sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;\n      output.format(format);\n      hasOutputFormat = true;\n    } else {\n      adtsScratch.skipBits(10);\n    }\n\n    adtsScratch.skipBits(4);\n    int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;\n    if (hasCrc) {\n      sampleSize -= CRC_SIZE;\n    }\n\n    setReadingSampleState(output, sampleDurationUs, 0, sampleSize);\n  }\n\n  /**\n   * Reads the rest of the sample\n   */\n  private void readSample(ParsableByteArray data) {\n    int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);\n    currentOutput.sampleData(data, bytesToRead);\n    bytesRead += bytesToRead;\n    if (bytesRead == sampleSize) {\n      currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n      timeUs += currentSampleDuration;\n      setFindingSampleState();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.SparseArray;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;\nimport com.google.android.exoplayer2.text.cea.Cea708InitializationData;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Default {@link TsPayloadReader.Factory} implementation.\n */\npublic final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {\n\n  /**\n   * Flags controlling elementary stream readers' behavior. Possible flag values are {@link\n   * #FLAG_ALLOW_NON_IDR_KEYFRAMES}, {@link #FLAG_IGNORE_AAC_STREAM}, {@link\n   * #FLAG_IGNORE_H264_STREAM}, {@link #FLAG_DETECT_ACCESS_UNITS}, {@link\n   * #FLAG_IGNORE_SPLICE_INFO_STREAM}, {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} and {@link\n   * #FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        FLAG_ALLOW_NON_IDR_KEYFRAMES,\n        FLAG_IGNORE_AAC_STREAM,\n        FLAG_IGNORE_H264_STREAM,\n        FLAG_DETECT_ACCESS_UNITS,\n        FLAG_IGNORE_SPLICE_INFO_STREAM,\n        FLAG_OVERRIDE_CAPTION_DESCRIPTORS,\n        FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS\n      })\n  public @interface Flags {}\n\n  /**\n   * When extracting H.264 samples, whether to treat samples consisting of non-IDR I slices as\n   * synchronization samples (key-frames).\n   */\n  public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;\n  /**\n   * Prevents the creation of {@link AdtsReader} and {@link LatmReader} instances. This flag should\n   * be enabled if the transport stream contains no packets for an AAC elementary stream that is\n   * declared in the PMT.\n   */\n  public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1;\n  /**\n   * Prevents the creation of {@link H264Reader} instances. This flag should be enabled if the\n   * transport stream contains no packets for an H.264 elementary stream that is declared in the\n   * PMT.\n   */\n  public static final int FLAG_IGNORE_H264_STREAM = 1 << 2;\n  /**\n   * When extracting H.264 samples, whether to split the input stream into access units (samples)\n   * based on slice headers. This flag should be disabled if the stream contains access unit\n   * delimiters (AUDs).\n   */\n  public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3;\n  /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */\n  public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4;\n  /**\n   * Whether the list of {@code closedCaptionFormats} passed to {@link\n   * DefaultTsPayloadReaderFactory#DefaultTsPayloadReaderFactory(int, List)} should be used in spite\n   * of any closed captions service descriptors. If this flag is disabled, {@code\n   * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.\n   */\n  public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;\n  /**\n   * Sets whether HDMV DTS audio streams will be handled. If this flag is set, SCTE subtitles will\n   * not be detected, as they share the same elementary stream type as HDMV DTS.\n   */\n  public static final int FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS = 1 << 6;\n\n  private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;\n\n  @Flags private final int flags;\n  private final List<Format> closedCaptionFormats;\n\n  public DefaultTsPayloadReaderFactory() {\n    this(0);\n  }\n\n  /**\n   * @param flags A combination of {@code FLAG_*} values that control the behavior of the created\n   *     readers.\n   */\n  public DefaultTsPayloadReaderFactory(@Flags int flags) {\n    this(\n        flags,\n        Collections.singletonList(\n            Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));\n  }\n\n  /**\n   * @param flags A combination of {@code FLAG_*} values that control the behavior of the created\n   *     readers.\n   * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with\n   *     embedded closed captions when no caption service descriptors are provided. If\n   *     {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides\n   *     any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a\n   *     closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will\n   *     be exposed.\n   */\n  public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {\n    this.flags = flags;\n    this.closedCaptionFormats = closedCaptionFormats;\n  }\n\n  @Override\n  public SparseArray<TsPayloadReader> createInitialPayloadReaders() {\n    return new SparseArray<>();\n  }\n\n  @Override\n  public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {\n    switch (streamType) {\n      case TsExtractor.TS_STREAM_TYPE_MPA:\n      case TsExtractor.TS_STREAM_TYPE_MPA_LSF:\n        return new PesReader(new MpegAudioReader(esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_AAC_ADTS:\n        return isSet(FLAG_IGNORE_AAC_STREAM)\n            ? null : new PesReader(new AdtsReader(false, esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_AAC_LATM:\n        return isSet(FLAG_IGNORE_AAC_STREAM)\n            ? null : new PesReader(new LatmReader(esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_AC3:\n      case TsExtractor.TS_STREAM_TYPE_E_AC3:\n        return new PesReader(new Ac3Reader(esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_AC4:\n        return new PesReader(new Ac4Reader(esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:\n        if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) {\n          return null;\n        }\n        // Fall through.\n      case TsExtractor.TS_STREAM_TYPE_DTS:\n        return new PesReader(new DtsReader(esInfo.language));\n      case TsExtractor.TS_STREAM_TYPE_H262:\n        return new PesReader(new H262Reader(buildUserDataReader(esInfo)));\n      case TsExtractor.TS_STREAM_TYPE_H264:\n        return isSet(FLAG_IGNORE_H264_STREAM) ? null\n            : new PesReader(new H264Reader(buildSeiReader(esInfo),\n                isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));\n      case TsExtractor.TS_STREAM_TYPE_H265:\n        return new PesReader(new H265Reader(buildSeiReader(esInfo)));\n      case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:\n        return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)\n            ? null : new SectionReader(new SpliceInfoSectionReader());\n      case TsExtractor.TS_STREAM_TYPE_ID3:\n        return new PesReader(new Id3Reader());\n      case TsExtractor.TS_STREAM_TYPE_DVBSUBS:\n        return new PesReader(\n            new DvbSubtitleReader(esInfo.dvbSubtitleInfos));\n      default:\n        return null;\n    }\n  }\n\n  /**\n   * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for\n   * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a\n   * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor\n   * is not present.\n   *\n   * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.\n   * @return A {@link SeiReader} for closed caption tracks.\n   */\n  private SeiReader buildSeiReader(EsInfo esInfo) {\n    return new SeiReader(getClosedCaptionFormats(esInfo));\n  }\n\n  /**\n   * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for\n   * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a\n   * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the\n   * descriptor is not present.\n   *\n   * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.\n   * @return A {@link UserDataReader} for closed caption tracks.\n   */\n  private UserDataReader buildUserDataReader(EsInfo esInfo) {\n    return new UserDataReader(getClosedCaptionFormats(esInfo));\n  }\n\n  /**\n   * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List<Format>} of {@link\n   * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link\n   * List<Format>} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is\n   * not present.\n   *\n   * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.\n   * @return A {@link List<Format>} containing list of closed caption formats.\n   */\n  private List<Format> getClosedCaptionFormats(EsInfo esInfo) {\n    if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {\n      return closedCaptionFormats;\n    }\n    ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);\n    List<Format> closedCaptionFormats = this.closedCaptionFormats;\n    while (scratchDescriptorData.bytesLeft() > 0) {\n      int descriptorTag = scratchDescriptorData.readUnsignedByte();\n      int descriptorLength = scratchDescriptorData.readUnsignedByte();\n      int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength;\n      if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) {\n        // Note: see ATSC A/65 for detailed information about the caption service descriptor.\n        closedCaptionFormats = new ArrayList<>();\n        int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F;\n        for (int i = 0; i < numberOfServices; i++) {\n          String language = scratchDescriptorData.readString(3);\n          int captionTypeByte = scratchDescriptorData.readUnsignedByte();\n          boolean isDigital = (captionTypeByte & 0x80) != 0;\n          String mimeType;\n          int accessibilityChannel;\n          if (isDigital) {\n            mimeType = MimeTypes.APPLICATION_CEA708;\n            accessibilityChannel = captionTypeByte & 0x3F;\n          } else {\n            mimeType = MimeTypes.APPLICATION_CEA608;\n            accessibilityChannel = 1;\n          }\n\n          // easy_reader(1), wide_aspect_ratio(1), reserved(6).\n          byte flags = (byte) scratchDescriptorData.readUnsignedByte();\n          // Skip reserved (8).\n          scratchDescriptorData.skipBytes(1);\n\n          List<byte[]> initializationData = null;\n          // The wide_aspect_ratio flag only has meaning for CEA-708.\n          if (isDigital) {\n            boolean isWideAspectRatio = (flags & 0x40) != 0;\n            initializationData = Cea708InitializationData.buildData(isWideAspectRatio);\n          }\n\n          closedCaptionFormats.add(\n              Format.createTextSampleFormat(\n                  /* id= */ null,\n                  mimeType,\n                  /* codecs= */ null,\n                  /* bitrate= */ Format.NO_VALUE,\n                  /* selectionFlags= */ 0,\n                  language,\n                  accessibilityChannel,\n                  /* drmInitData= */ null,\n                  Format.OFFSET_SAMPLE_RELATIVE,\n                  initializationData));\n        }\n      } else {\n        // Unknown descriptor. Ignore.\n      }\n      scratchDescriptorData.setPosition(nextDescriptorPosition);\n    }\n\n    return closedCaptionFormats;\n  }\n\n  private boolean isSet(@Flags int flag) {\n    return (flags & flag) != 0;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.DtsUtil;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Parses a continuous DTS byte stream and extracts individual samples.\n */\npublic final class DtsReader implements ElementaryStreamReader {\n\n  private static final int STATE_FINDING_SYNC = 0;\n  private static final int STATE_READING_HEADER = 1;\n  private static final int STATE_READING_SAMPLE = 2;\n\n  private static final int HEADER_SIZE = 18;\n\n  private final ParsableByteArray headerScratchBytes;\n  private final String language;\n\n  private String formatId;\n  private TrackOutput output;\n\n  private int state;\n  private int bytesRead;\n\n  // Used to find the header.\n  private int syncBytes;\n\n  // Used when parsing the header.\n  private long sampleDurationUs;\n  private Format format;\n  private int sampleSize;\n\n  // Used when reading the samples.\n  private long timeUs;\n\n  /**\n   * Constructs a new reader for DTS elementary streams.\n   *\n   * @param language Track language.\n   */\n  public DtsReader(String language) {\n    headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);\n    state = STATE_FINDING_SYNC;\n    this.language = language;\n  }\n\n  @Override\n  public void seek() {\n    state = STATE_FINDING_SYNC;\n    bytesRead = 0;\n    syncBytes = 0;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_SYNC:\n          if (skipToNextSync(data)) {\n            state = STATE_READING_HEADER;\n          }\n          break;\n        case STATE_READING_HEADER:\n          if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {\n            parseHeader();\n            headerScratchBytes.setPosition(0);\n            output.sampleData(headerScratchBytes, HEADER_SIZE);\n            state = STATE_READING_SAMPLE;\n          }\n          break;\n        case STATE_READING_SAMPLE:\n          int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);\n          output.sampleData(data, bytesToRead);\n          bytesRead += bytesToRead;\n          if (bytesRead == sampleSize) {\n            output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n            timeUs += sampleDurationUs;\n            state = STATE_FINDING_SYNC;\n          }\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed\n   * that the data should be written into {@code target} starting from an offset of zero.\n   *\n   * @param source The source from which to read.\n   * @param target The target into which data is to be read.\n   * @param targetLength The target length of the read.\n   * @return Whether the target length was reached.\n   */\n  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {\n    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);\n    source.readBytes(target, bytesRead, bytesToRead);\n    bytesRead += bytesToRead;\n    return bytesRead == targetLength;\n  }\n\n  /**\n   * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately\n   * follows it. If SYNC was not located, the position is advanced to the limit.\n   *\n   * @param pesBuffer The buffer whose position should be advanced.\n   * @return Whether SYNC was found.\n   */\n  private boolean skipToNextSync(ParsableByteArray pesBuffer) {\n    while (pesBuffer.bytesLeft() > 0) {\n      syncBytes <<= 8;\n      syncBytes |= pesBuffer.readUnsignedByte();\n      if (DtsUtil.isSyncWord(syncBytes)) {\n        headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF);\n        headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF);\n        headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF);\n        headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF);\n        bytesRead = 4;\n        syncBytes = 0;\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Parses the sample header.\n   */\n  private void parseHeader() {\n    byte[] frameData = headerScratchBytes.data;\n    if (format == null) {\n      format = DtsUtil.parseDtsFormat(frameData, formatId, language, null);\n      output.format(format);\n    }\n    sampleSize = DtsUtil.getDtsFrameSize(frameData);\n    // In this class a sample is an access unit (frame in DTS), but the format's sample rate\n    // specifies the number of PCM audio samples per second.\n    sampleDurationUs = (int) (C.MICROS_PER_SECOND\n        * DtsUtil.parseDtsAudioSampleCount(frameData) / format.sampleRate);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Parses DVB subtitle data and extracts individual frames.\n */\npublic final class DvbSubtitleReader implements ElementaryStreamReader {\n\n  private final List<DvbSubtitleInfo> subtitleInfos;\n  private final TrackOutput[] outputs;\n\n  private boolean writingSample;\n  private int bytesToCheck;\n  private int sampleBytesWritten;\n  private long sampleTimeUs;\n\n  /**\n   * @param subtitleInfos Information about the DVB subtitles associated to the stream.\n   */\n  public DvbSubtitleReader(List<DvbSubtitleInfo> subtitleInfos) {\n    this.subtitleInfos = subtitleInfos;\n    outputs = new TrackOutput[subtitleInfos.size()];\n  }\n\n  @Override\n  public void seek() {\n    writingSample = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    for (int i = 0; i < outputs.length; i++) {\n      DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i);\n      idGenerator.generateNewId();\n      TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);\n      output.format(\n          Format.createImageSampleFormat(\n              idGenerator.getFormatId(),\n              MimeTypes.APPLICATION_DVBSUBS,\n              null,\n              Format.NO_VALUE,\n              0,\n              Collections.singletonList(subtitleInfo.initializationData),\n              subtitleInfo.language,\n              null));\n      outputs[i] = output;\n    }\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {\n      return;\n    }\n    writingSample = true;\n    sampleTimeUs = pesTimeUs;\n    sampleBytesWritten = 0;\n    bytesToCheck = 2;\n  }\n\n  @Override\n  public void packetFinished() {\n    if (writingSample) {\n      for (TrackOutput output : outputs) {\n        output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);\n      }\n      writingSample = false;\n    }\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    if (writingSample) {\n      if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) {\n        // Failed to check data_identifier\n        return;\n      }\n      if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) {\n        // Check and discard the subtitle_stream_id\n        return;\n      }\n      int dataPosition = data.getPosition();\n      int bytesAvailable = data.bytesLeft();\n      for (TrackOutput output : outputs) {\n        data.setPosition(dataPosition);\n        output.sampleData(data, bytesAvailable);\n      }\n      sampleBytesWritten += bytesAvailable;\n    }\n  }\n\n  private boolean checkNextByte(ParsableByteArray data, int expectedValue) {\n    if (data.bytesLeft() == 0) {\n      return false;\n    }\n    if (data.readUnsignedByte() != expectedValue) {\n      writingSample = false;\n    }\n    bytesToCheck--;\n    return writingSample;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Extracts individual samples from an elementary media stream, preserving original order.\n */\npublic interface ElementaryStreamReader {\n\n  /**\n   * Notifies the reader that a seek has occurred.\n   */\n  void seek();\n\n  /**\n   * Initializes the reader by providing outputs and ids for the tracks.\n   *\n   * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.\n   * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the\n   *     {@link TrackOutput}s.\n   */\n  void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator);\n\n  /**\n   * Called when a packet starts.\n   *\n   * @param pesTimeUs The timestamp associated with the packet.\n   * @param flags See {@link TsPayloadReader.Flags}.\n   */\n  void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags);\n\n  /**\n   * Consumes (possibly partial) data from the current packet.\n   *\n   * @param data The data to consume.\n   * @throws ParserException If the data could not be parsed.\n   */\n  void consume(ParsableByteArray data) throws ParserException;\n\n  /**\n   * Called when a packet ends.\n   */\n  void packetFinished();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Arrays;\nimport java.util.Collections;\n\n/**\n * Parses a continuous H262 byte stream and extracts individual frames.\n */\npublic final class H262Reader implements ElementaryStreamReader {\n\n  private static final int START_PICTURE = 0x00;\n  private static final int START_SEQUENCE_HEADER = 0xB3;\n  private static final int START_EXTENSION = 0xB5;\n  private static final int START_GROUP = 0xB8;\n  private static final int START_USER_DATA = 0xB2;\n\n  private String formatId;\n  private TrackOutput output;\n\n  // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.\n  private static final double[] FRAME_RATE_VALUES = new double[] {\n      24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};\n\n  // State that should not be reset on seek.\n  private boolean hasOutputFormat;\n  private long frameDurationUs;\n\n  private final UserDataReader userDataReader;\n  private final ParsableByteArray userDataParsable;\n\n  // State that should be reset on seek.\n  private final boolean[] prefixFlags;\n  private final CsdBuffer csdBuffer;\n  private final NalUnitTargetBuffer userData;\n  private long totalBytesWritten;\n  private boolean startedFirstSample;\n\n  // Per packet state that gets reset at the start of each packet.\n  private long pesTimeUs;\n\n  // Per sample state that gets reset at the start of each sample.\n  private long samplePosition;\n  private long sampleTimeUs;\n  private boolean sampleIsKeyframe;\n  private boolean sampleHasPicture;\n\n  public H262Reader() {\n    this(null);\n  }\n\n  /* package */ H262Reader(UserDataReader userDataReader) {\n    this.userDataReader = userDataReader;\n    prefixFlags = new boolean[4];\n    csdBuffer = new CsdBuffer(128);\n    if (userDataReader != null) {\n      userData = new NalUnitTargetBuffer(START_USER_DATA, 128);\n      userDataParsable = new ParsableByteArray();\n    } else {\n      userData = null;\n      userDataParsable = null;\n    }\n  }\n\n  @Override\n  public void seek() {\n    NalUnitUtil.clearPrefixFlags(prefixFlags);\n    csdBuffer.reset();\n    if (userDataReader != null) {\n      userData.reset();\n    }\n    totalBytesWritten = 0;\n    startedFirstSample = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);\n    if (userDataReader != null) {\n      userDataReader.createTracks(extractorOutput, idGenerator);\n    }\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    // TODO (Internal b/32267012): Consider using random access indicator.\n    this.pesTimeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    int offset = data.getPosition();\n    int limit = data.limit();\n    byte[] dataArray = data.data;\n\n    // Append the data to the buffer.\n    totalBytesWritten += data.bytesLeft();\n    output.sampleData(data, data.bytesLeft());\n\n    while (true) {\n      int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);\n\n      if (startCodeOffset == limit) {\n        // We've scanned to the end of the data without finding another start code.\n        if (!hasOutputFormat) {\n          csdBuffer.onData(dataArray, offset, limit);\n        }\n        if (userDataReader != null) {\n          userData.appendToNalUnit(dataArray, offset, limit);\n        }\n        return;\n      }\n\n      // We've found a start code with the following value.\n      int startCodeValue = data.data[startCodeOffset + 3] & 0xFF;\n      // This is the number of bytes from the current offset to the start of the next start\n      // code. It may be negative if the start code started in the previously consumed data.\n      int lengthToStartCode = startCodeOffset - offset;\n\n      if (!hasOutputFormat) {\n        if (lengthToStartCode > 0) {\n          csdBuffer.onData(dataArray, offset, startCodeOffset);\n        }\n        // This is the number of bytes belonging to the next start code that have already been\n        // passed to csdBuffer.\n        int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0;\n        if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) {\n          // The csd data is complete, so we can decode and output the media format.\n          Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);\n          output.format(result.first);\n          frameDurationUs = result.second;\n          hasOutputFormat = true;\n        }\n      }\n      if (userDataReader != null) {\n        int bytesAlreadyPassed = 0;\n        if (lengthToStartCode > 0) {\n          userData.appendToNalUnit(dataArray, offset, startCodeOffset);\n        } else {\n          bytesAlreadyPassed = -lengthToStartCode;\n        }\n\n        if (userData.endNalUnit(bytesAlreadyPassed)) {\n          int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);\n          userDataParsable.reset(userData.nalData, unescapedLength);\n          userDataReader.consume(sampleTimeUs, userDataParsable);\n        }\n\n        if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) {\n          userData.startNalUnit(startCodeValue);\n        }\n      }\n      if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) {\n        int bytesWrittenPastStartCode = limit - startCodeOffset;\n        if (startedFirstSample && sampleHasPicture && hasOutputFormat) {\n          // Output the sample.\n          @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;\n          int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode;\n          output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null);\n        }\n        if (!startedFirstSample || sampleHasPicture) {\n          // Start the next sample.\n          samplePosition = totalBytesWritten - bytesWrittenPastStartCode;\n          sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs\n              : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0);\n          sampleIsKeyframe = false;\n          pesTimeUs = C.TIME_UNSET;\n          startedFirstSample = true;\n        }\n        sampleHasPicture = startCodeValue == START_PICTURE;\n      } else if (startCodeValue == START_GROUP) {\n        sampleIsKeyframe = true;\n      }\n\n      offset = startCodeOffset + 3;\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Parses the {@link Format} and frame duration from a csd buffer.\n   *\n   * @param csdBuffer The csd buffer.\n   * @param formatId The id for the generated format. May be null.\n   * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or\n   *     0 if the duration could not be determined.\n   */\n  private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {\n    byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);\n\n    int firstByte = csdData[4] & 0xFF;\n    int secondByte = csdData[5] & 0xFF;\n    int thirdByte = csdData[6] & 0xFF;\n    int width = (firstByte << 4) | (secondByte >> 4);\n    int height = (secondByte & 0x0F) << 8 | thirdByte;\n\n    float pixelWidthHeightRatio = 1f;\n    int aspectRatioCode = (csdData[7] & 0xF0) >> 4;\n    switch(aspectRatioCode) {\n      case 2:\n        pixelWidthHeightRatio = (4 * height) / (float) (3 * width);\n        break;\n      case 3:\n        pixelWidthHeightRatio = (16 * height) / (float) (9 * width);\n        break;\n      case 4:\n        pixelWidthHeightRatio = (121 * height) / (float) (100 * width);\n        break;\n      default:\n        // Do nothing.\n        break;\n    }\n\n    Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null,\n        Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE,\n        Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null);\n\n    long frameDurationUs = 0;\n    int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1;\n    if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) {\n      double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne];\n      int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition;\n      int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5;\n      int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F);\n      if (frameRateExtensionN != frameRateExtensionD) {\n        frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1);\n      }\n      frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate);\n    }\n\n    return Pair.create(format, frameDurationUs);\n  }\n\n  private static final class CsdBuffer {\n\n    private static final byte[] START_CODE = new byte[] {0, 0, 1};\n\n    private boolean isFilling;\n\n    public int length;\n    public int sequenceExtensionPosition;\n    public byte[] data;\n\n    public CsdBuffer(int initialCapacity) {\n      data = new byte[initialCapacity];\n    }\n\n    /**\n     * Resets the buffer, clearing any data that it holds.\n     */\n    public void reset() {\n      isFilling = false;\n      length = 0;\n      sequenceExtensionPosition = 0;\n    }\n\n    /**\n     * Called when a start code is encountered in the stream.\n     *\n     * @param startCodeValue The start code value.\n     * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to\n     *     {@link #onData(byte[], int, int)}, or 0.\n     * @return Whether the csd data is now complete. If true is returned, neither\n     *     this method nor {@link #onData(byte[], int, int)} should be called again without an\n     *     interleaving call to {@link #reset()}.\n     */\n    public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) {\n      if (isFilling) {\n        length -= bytesAlreadyPassed;\n        if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) {\n          sequenceExtensionPosition = length;\n        } else {\n          isFilling = false;\n          return true;\n        }\n      } else if (startCodeValue == START_SEQUENCE_HEADER) {\n        isFilling = true;\n      }\n      onData(START_CODE, 0, START_CODE.length);\n      return false;\n    }\n\n    /**\n     * Called to pass stream data.\n     *\n     * @param newData Holds the data being passed.\n     * @param offset The offset of the data in {@code data}.\n     * @param limit The limit (exclusive) of the data in {@code data}.\n     */\n    public void onData(byte[] newData, int offset, int limit) {\n      if (!isFilling) {\n        return;\n      }\n      int readLength = limit - offset;\n      if (data.length < length + readLength) {\n        data = Arrays.copyOf(data, (length + readLength) * 2);\n      }\n      System.arraycopy(newData, offset, data, length, readLength);\n      length += readLength;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR;\n\nimport android.util.SparseArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.NalUnitUtil.SpsData;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.ParsableNalUnitBitArray;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * Parses a continuous H264 byte stream and extracts individual frames.\n */\npublic final class H264Reader implements ElementaryStreamReader {\n\n  private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information\n  private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set\n  private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set\n\n  private final SeiReader seiReader;\n  private final boolean allowNonIdrKeyframes;\n  private final boolean detectAccessUnits;\n  private final NalUnitTargetBuffer sps;\n  private final NalUnitTargetBuffer pps;\n  private final NalUnitTargetBuffer sei;\n  private long totalBytesWritten;\n  private final boolean[] prefixFlags;\n\n  private String formatId;\n  private TrackOutput output;\n  private SampleReader sampleReader;\n\n  // State that should not be reset on seek.\n  private boolean hasOutputFormat;\n\n  // Per PES packet state that gets reset at the start of each PES packet.\n  private long pesTimeUs;\n\n  // State inherited from the TS packet header.\n  private boolean randomAccessIndicator;\n\n  // Scratch variables to avoid allocations.\n  private final ParsableByteArray seiWrapper;\n\n  /**\n   * @param seiReader An SEI reader for consuming closed caption channels.\n   * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as\n   *     synchronization samples (key-frames).\n   * @param detectAccessUnits Whether to split the input stream into access units (samples) based on\n   *     slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).\n   */\n  public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) {\n    this.seiReader = seiReader;\n    this.allowNonIdrKeyframes = allowNonIdrKeyframes;\n    this.detectAccessUnits = detectAccessUnits;\n    prefixFlags = new boolean[3];\n    sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);\n    pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);\n    sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);\n    seiWrapper = new ParsableByteArray();\n  }\n\n  @Override\n  public void seek() {\n    NalUnitUtil.clearPrefixFlags(prefixFlags);\n    sps.reset();\n    pps.reset();\n    sei.reset();\n    sampleReader.reset();\n    totalBytesWritten = 0;\n    randomAccessIndicator = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);\n    sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);\n    seiReader.createTracks(extractorOutput, idGenerator);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    this.pesTimeUs = pesTimeUs;\n    randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    int offset = data.getPosition();\n    int limit = data.limit();\n    byte[] dataArray = data.data;\n\n    // Append the data to the buffer.\n    totalBytesWritten += data.bytesLeft();\n    output.sampleData(data, data.bytesLeft());\n\n    // Scan the appended data, processing NAL units as they are encountered\n    while (true) {\n      int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);\n\n      if (nalUnitOffset == limit) {\n        // We've scanned to the end of the data without finding the start of another NAL unit.\n        nalUnitData(dataArray, offset, limit);\n        return;\n      }\n\n      // We've seen the start of a NAL unit of the following type.\n      int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);\n\n      // This is the number of bytes from the current offset to the start of the next NAL unit.\n      // It may be negative if the NAL unit started in the previously consumed data.\n      int lengthToNalUnit = nalUnitOffset - offset;\n      if (lengthToNalUnit > 0) {\n        nalUnitData(dataArray, offset, nalUnitOffset);\n      }\n      int bytesWrittenPastPosition = limit - nalUnitOffset;\n      long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;\n      // Indicate the end of the previous NAL unit. If the length to the start of the next unit\n      // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes\n      // when notifying that the unit has ended.\n      endNalUnit(absolutePosition, bytesWrittenPastPosition,\n          lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);\n      // Indicate the start of the next NAL unit.\n      startNalUnit(absolutePosition, nalUnitType, pesTimeUs);\n      // Continue scanning the data.\n      offset = nalUnitOffset + 3;\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {\n    if (!hasOutputFormat || sampleReader.needsSpsPps()) {\n      sps.startNalUnit(nalUnitType);\n      pps.startNalUnit(nalUnitType);\n    }\n    sei.startNalUnit(nalUnitType);\n    sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);\n  }\n\n  private void nalUnitData(byte[] dataArray, int offset, int limit) {\n    if (!hasOutputFormat || sampleReader.needsSpsPps()) {\n      sps.appendToNalUnit(dataArray, offset, limit);\n      pps.appendToNalUnit(dataArray, offset, limit);\n    }\n    sei.appendToNalUnit(dataArray, offset, limit);\n    sampleReader.appendToNalUnit(dataArray, offset, limit);\n  }\n\n  private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {\n    if (!hasOutputFormat || sampleReader.needsSpsPps()) {\n      sps.endNalUnit(discardPadding);\n      pps.endNalUnit(discardPadding);\n      if (!hasOutputFormat) {\n        if (sps.isCompleted() && pps.isCompleted()) {\n          List<byte[]> initializationData = new ArrayList<>();\n          initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength));\n          initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));\n          SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);\n          NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);\n          output.format(\n              Format.createVideoSampleFormat(\n                  formatId,\n                  MimeTypes.VIDEO_H264,\n                  CodecSpecificDataUtil.buildAvcCodecString(\n                      spsData.profileIdc,\n                      spsData.constraintsFlagsAndReservedZero2Bits,\n                      spsData.levelIdc),\n                  /* bitrate= */ Format.NO_VALUE,\n                  /* maxInputSize= */ Format.NO_VALUE,\n                  spsData.width,\n                  spsData.height,\n                  /* frameRate= */ Format.NO_VALUE,\n                  initializationData,\n                  /* rotationDegrees= */ Format.NO_VALUE,\n                  spsData.pixelWidthAspectRatio,\n                  /* drmInitData= */ null));\n          hasOutputFormat = true;\n          sampleReader.putSps(spsData);\n          sampleReader.putPps(ppsData);\n          sps.reset();\n          pps.reset();\n        }\n      } else if (sps.isCompleted()) {\n        SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);\n        sampleReader.putSps(spsData);\n        sps.reset();\n      } else if (pps.isCompleted()) {\n        NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);\n        sampleReader.putPps(ppsData);\n        pps.reset();\n      }\n    }\n    if (sei.endNalUnit(discardPadding)) {\n      int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);\n      seiWrapper.reset(sei.nalData, unescapedLength);\n      seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.\n      seiReader.consume(pesTimeUs, seiWrapper);\n    }\n    boolean sampleIsKeyFrame =\n        sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);\n    if (sampleIsKeyFrame) {\n      // This is either an IDR frame or the first I-frame since the random access indicator, so mark\n      // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as\n      // keyframes until we see another random access indicator.\n      randomAccessIndicator = false;\n    }\n  }\n\n  /** Consumes a stream of NAL units and outputs samples. */\n  private static final class SampleReader {\n\n    private static final int DEFAULT_BUFFER_SIZE = 128;\n\n    private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture\n    private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A\n    private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture\n    private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter\n\n    private final TrackOutput output;\n    private final boolean allowNonIdrKeyframes;\n    private final boolean detectAccessUnits;\n    private final SparseArray<SpsData> sps;\n    private final SparseArray<NalUnitUtil.PpsData> pps;\n    private final ParsableNalUnitBitArray bitArray;\n\n    private byte[] buffer;\n    private int bufferLength;\n\n    // Per NAL unit state. A sample consists of one or more NAL units.\n    private int nalUnitType;\n    private long nalUnitStartPosition;\n    private boolean isFilling;\n    private long nalUnitTimeUs;\n    private SliceHeaderData previousSliceHeader;\n    private SliceHeaderData sliceHeader;\n\n    // Per sample state that gets reset at the start of each sample.\n    private boolean readingSample;\n    private long samplePosition;\n    private long sampleTimeUs;\n    private boolean sampleIsKeyframe;\n\n    public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes,\n        boolean detectAccessUnits) {\n      this.output = output;\n      this.allowNonIdrKeyframes = allowNonIdrKeyframes;\n      this.detectAccessUnits = detectAccessUnits;\n      sps = new SparseArray<>();\n      pps = new SparseArray<>();\n      previousSliceHeader = new SliceHeaderData();\n      sliceHeader = new SliceHeaderData();\n      buffer = new byte[DEFAULT_BUFFER_SIZE];\n      bitArray = new ParsableNalUnitBitArray(buffer, 0, 0);\n      reset();\n    }\n\n    public boolean needsSpsPps() {\n      return detectAccessUnits;\n    }\n\n    public void putSps(SpsData spsData) {\n      sps.append(spsData.seqParameterSetId, spsData);\n    }\n\n    public void putPps(NalUnitUtil.PpsData ppsData) {\n      pps.append(ppsData.picParameterSetId, ppsData);\n    }\n\n    public void reset() {\n      isFilling = false;\n      readingSample = false;\n      sliceHeader.clear();\n    }\n\n    public void startNalUnit(long position, int type, long pesTimeUs) {\n      nalUnitType = type;\n      nalUnitTimeUs = pesTimeUs;\n      nalUnitStartPosition = position;\n      if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR)\n          || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR\n              || nalUnitType == NAL_UNIT_TYPE_NON_IDR\n              || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) {\n        // Store the previous header and prepare to populate the new one.\n        SliceHeaderData newSliceHeader = previousSliceHeader;\n        previousSliceHeader = sliceHeader;\n        sliceHeader = newSliceHeader;\n        sliceHeader.clear();\n        bufferLength = 0;\n        isFilling = true;\n      }\n    }\n\n    /**\n     * Called to pass stream data. The data passed should not include the 3 byte start code.\n     *\n     * @param data Holds the data being passed.\n     * @param offset The offset of the data in {@code data}.\n     * @param limit The limit (exclusive) of the data in {@code data}.\n     */\n    public void appendToNalUnit(byte[] data, int offset, int limit) {\n      if (!isFilling) {\n        return;\n      }\n      int readLength = limit - offset;\n      if (buffer.length < bufferLength + readLength) {\n        buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2);\n      }\n      System.arraycopy(data, offset, buffer, bufferLength, readLength);\n      bufferLength += readLength;\n\n      bitArray.reset(buffer, 0, bufferLength);\n      if (!bitArray.canReadBits(8)) {\n        return;\n      }\n      bitArray.skipBit(); // forbidden_zero_bit\n      int nalRefIdc = bitArray.readBits(2);\n      bitArray.skipBits(5); // nal_unit_type\n\n      // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013)\n      // subsection 7.3.3.\n      if (!bitArray.canReadExpGolombCodedNum()) {\n        return;\n      }\n      bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice\n      if (!bitArray.canReadExpGolombCodedNum()) {\n        return;\n      }\n      int sliceType = bitArray.readUnsignedExpGolombCodedInt();\n      if (!detectAccessUnits) {\n        // There are AUDs in the stream so the rest of the header can be ignored.\n        isFilling = false;\n        sliceHeader.setSliceType(sliceType);\n        return;\n      }\n      if (!bitArray.canReadExpGolombCodedNum()) {\n        return;\n      }\n      int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt();\n      if (pps.indexOfKey(picParameterSetId) < 0) {\n        // We have not seen the PPS yet, so don't try to decode the slice header.\n        isFilling = false;\n        return;\n      }\n      NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId);\n      SpsData spsData = sps.get(ppsData.seqParameterSetId);\n      if (spsData.separateColorPlaneFlag) {\n        if (!bitArray.canReadBits(2)) {\n          return;\n        }\n        bitArray.skipBits(2); // colour_plane_id\n      }\n      if (!bitArray.canReadBits(spsData.frameNumLength)) {\n        return;\n      }\n      boolean fieldPicFlag = false;\n      boolean bottomFieldFlagPresent = false;\n      boolean bottomFieldFlag = false;\n      int frameNum = bitArray.readBits(spsData.frameNumLength);\n      if (!spsData.frameMbsOnlyFlag) {\n        if (!bitArray.canReadBits(1)) {\n          return;\n        }\n        fieldPicFlag = bitArray.readBit();\n        if (fieldPicFlag) {\n          if (!bitArray.canReadBits(1)) {\n            return;\n          }\n          bottomFieldFlag = bitArray.readBit();\n          bottomFieldFlagPresent = true;\n        }\n      }\n      boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR;\n      int idrPicId = 0;\n      if (idrPicFlag) {\n        if (!bitArray.canReadExpGolombCodedNum()) {\n          return;\n        }\n        idrPicId = bitArray.readUnsignedExpGolombCodedInt();\n      }\n      int picOrderCntLsb = 0;\n      int deltaPicOrderCntBottom = 0;\n      int deltaPicOrderCnt0 = 0;\n      int deltaPicOrderCnt1 = 0;\n      if (spsData.picOrderCountType == 0) {\n        if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) {\n          return;\n        }\n        picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength);\n        if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {\n          if (!bitArray.canReadExpGolombCodedNum()) {\n            return;\n          }\n          deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt();\n        }\n      } else if (spsData.picOrderCountType == 1\n          && !spsData.deltaPicOrderAlwaysZeroFlag) {\n        if (!bitArray.canReadExpGolombCodedNum()) {\n          return;\n        }\n        deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt();\n        if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {\n          if (!bitArray.canReadExpGolombCodedNum()) {\n            return;\n          }\n          deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt();\n        }\n      }\n      sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag,\n          bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb,\n          deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1);\n      isFilling = false;\n    }\n\n    public boolean endNalUnit(\n        long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {\n      if (nalUnitType == NAL_UNIT_TYPE_AUD\n          || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {\n        // If the NAL unit ending is the start of a new sample, output the previous one.\n        if (hasOutputFormat && readingSample) {\n          int nalUnitLength = (int) (position - nalUnitStartPosition);\n          outputSample(offset + nalUnitLength);\n        }\n        samplePosition = nalUnitStartPosition;\n        sampleTimeUs = nalUnitTimeUs;\n        sampleIsKeyframe = false;\n        readingSample = true;\n      }\n      boolean treatIFrameAsKeyframe =\n          allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;\n      sampleIsKeyframe |=\n          nalUnitType == NAL_UNIT_TYPE_IDR\n              || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);\n      return sampleIsKeyframe;\n    }\n\n    private void outputSample(int offset) {\n      @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;\n      int size = (int) (nalUnitStartPosition - samplePosition);\n      output.sampleMetadata(sampleTimeUs, flags, size, offset, null);\n    }\n\n    private static final class SliceHeaderData {\n\n      private static final int SLICE_TYPE_I = 2;\n      private static final int SLICE_TYPE_ALL_I = 7;\n\n      private boolean isComplete;\n      private boolean hasSliceType;\n\n      private SpsData spsData;\n      private int nalRefIdc;\n      private int sliceType;\n      private int frameNum;\n      private int picParameterSetId;\n      private boolean fieldPicFlag;\n      private boolean bottomFieldFlagPresent;\n      private boolean bottomFieldFlag;\n      private boolean idrPicFlag;\n      private int idrPicId;\n      private int picOrderCntLsb;\n      private int deltaPicOrderCntBottom;\n      private int deltaPicOrderCnt0;\n      private int deltaPicOrderCnt1;\n\n      public void clear() {\n        hasSliceType = false;\n        isComplete = false;\n      }\n\n      public void setSliceType(int sliceType) {\n        this.sliceType = sliceType;\n        hasSliceType = true;\n      }\n\n      public void setAll(\n          SpsData spsData,\n          int nalRefIdc,\n          int sliceType,\n          int frameNum,\n          int picParameterSetId,\n          boolean fieldPicFlag,\n          boolean bottomFieldFlagPresent,\n          boolean bottomFieldFlag,\n          boolean idrPicFlag,\n          int idrPicId,\n          int picOrderCntLsb,\n          int deltaPicOrderCntBottom,\n          int deltaPicOrderCnt0,\n          int deltaPicOrderCnt1) {\n        this.spsData = spsData;\n        this.nalRefIdc = nalRefIdc;\n        this.sliceType = sliceType;\n        this.frameNum = frameNum;\n        this.picParameterSetId = picParameterSetId;\n        this.fieldPicFlag = fieldPicFlag;\n        this.bottomFieldFlagPresent = bottomFieldFlagPresent;\n        this.bottomFieldFlag = bottomFieldFlag;\n        this.idrPicFlag = idrPicFlag;\n        this.idrPicId = idrPicId;\n        this.picOrderCntLsb = picOrderCntLsb;\n        this.deltaPicOrderCntBottom = deltaPicOrderCntBottom;\n        this.deltaPicOrderCnt0 = deltaPicOrderCnt0;\n        this.deltaPicOrderCnt1 = deltaPicOrderCnt1;\n        isComplete = true;\n        hasSliceType = true;\n      }\n\n      public boolean isISlice() {\n        return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I);\n      }\n\n      private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {\n        // See ISO 14496-10 subsection 7.4.1.2.4.\n        return isComplete\n            && (!other.isComplete\n                || frameNum != other.frameNum\n                || picParameterSetId != other.picParameterSetId\n                || fieldPicFlag != other.fieldPicFlag\n                || (bottomFieldFlagPresent\n                    && other.bottomFieldFlagPresent\n                    && bottomFieldFlag != other.bottomFieldFlag)\n                || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))\n                || (spsData.picOrderCountType == 0\n                    && other.spsData.picOrderCountType == 0\n                    && (picOrderCntLsb != other.picOrderCntLsb\n                        || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))\n                || (spsData.picOrderCountType == 1\n                    && other.spsData.picOrderCountType == 1\n                    && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0\n                        || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))\n                || idrPicFlag != other.idrPicFlag\n                || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.ParsableNalUnitBitArray;\nimport java.util.Collections;\n\n/**\n * Parses a continuous H.265 byte stream and extracts individual frames.\n */\npublic final class H265Reader implements ElementaryStreamReader {\n\n  private static final String TAG = \"H265Reader\";\n\n  // nal_unit_type values from H.265/HEVC (2014) Table 7-1.\n  private static final int RASL_R = 9;\n  private static final int BLA_W_LP = 16;\n  private static final int CRA_NUT = 21;\n  private static final int VPS_NUT = 32;\n  private static final int SPS_NUT = 33;\n  private static final int PPS_NUT = 34;\n  private static final int PREFIX_SEI_NUT = 39;\n  private static final int SUFFIX_SEI_NUT = 40;\n\n  private final SeiReader seiReader;\n\n  private String formatId;\n  private TrackOutput output;\n  private SampleReader sampleReader;\n\n  // State that should not be reset on seek.\n  private boolean hasOutputFormat;\n\n  // State that should be reset on seek.\n  private final boolean[] prefixFlags;\n  private final NalUnitTargetBuffer vps;\n  private final NalUnitTargetBuffer sps;\n  private final NalUnitTargetBuffer pps;\n  private final NalUnitTargetBuffer prefixSei;\n  private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed?\n  private long totalBytesWritten;\n\n  // Per packet state that gets reset at the start of each packet.\n  private long pesTimeUs;\n\n  // Scratch variables to avoid allocations.\n  private final ParsableByteArray seiWrapper;\n\n  /**\n   * @param seiReader An SEI reader for consuming closed caption channels.\n   */\n  public H265Reader(SeiReader seiReader) {\n    this.seiReader = seiReader;\n    prefixFlags = new boolean[3];\n    vps = new NalUnitTargetBuffer(VPS_NUT, 128);\n    sps = new NalUnitTargetBuffer(SPS_NUT, 128);\n    pps = new NalUnitTargetBuffer(PPS_NUT, 128);\n    prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128);\n    suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128);\n    seiWrapper = new ParsableByteArray();\n  }\n\n  @Override\n  public void seek() {\n    NalUnitUtil.clearPrefixFlags(prefixFlags);\n    vps.reset();\n    sps.reset();\n    pps.reset();\n    prefixSei.reset();\n    suffixSei.reset();\n    sampleReader.reset();\n    totalBytesWritten = 0;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);\n    sampleReader = new SampleReader(output);\n    seiReader.createTracks(extractorOutput, idGenerator);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    // TODO (Internal b/32267012): Consider using random access indicator.\n    this.pesTimeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    while (data.bytesLeft() > 0) {\n      int offset = data.getPosition();\n      int limit = data.limit();\n      byte[] dataArray = data.data;\n\n      // Append the data to the buffer.\n      totalBytesWritten += data.bytesLeft();\n      output.sampleData(data, data.bytesLeft());\n\n      // Scan the appended data, processing NAL units as they are encountered\n      while (offset < limit) {\n        int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);\n\n        if (nalUnitOffset == limit) {\n          // We've scanned to the end of the data without finding the start of another NAL unit.\n          nalUnitData(dataArray, offset, limit);\n          return;\n        }\n\n        // We've seen the start of a NAL unit of the following type.\n        int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset);\n\n        // This is the number of bytes from the current offset to the start of the next NAL unit.\n        // It may be negative if the NAL unit started in the previously consumed data.\n        int lengthToNalUnit = nalUnitOffset - offset;\n        if (lengthToNalUnit > 0) {\n          nalUnitData(dataArray, offset, nalUnitOffset);\n        }\n\n        int bytesWrittenPastPosition = limit - nalUnitOffset;\n        long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;\n        // Indicate the end of the previous NAL unit. If the length to the start of the next unit\n        // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes\n        // when notifying that the unit has ended.\n        endNalUnit(absolutePosition, bytesWrittenPastPosition,\n            lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);\n        // Indicate the start of the next NAL unit.\n        startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs);\n        // Continue scanning the data.\n        offset = nalUnitOffset + 3;\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {\n    if (hasOutputFormat) {\n      sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);\n    } else {\n      vps.startNalUnit(nalUnitType);\n      sps.startNalUnit(nalUnitType);\n      pps.startNalUnit(nalUnitType);\n    }\n    prefixSei.startNalUnit(nalUnitType);\n    suffixSei.startNalUnit(nalUnitType);\n  }\n\n  private void nalUnitData(byte[] dataArray, int offset, int limit) {\n    if (hasOutputFormat) {\n      sampleReader.readNalUnitData(dataArray, offset, limit);\n    } else {\n      vps.appendToNalUnit(dataArray, offset, limit);\n      sps.appendToNalUnit(dataArray, offset, limit);\n      pps.appendToNalUnit(dataArray, offset, limit);\n    }\n    prefixSei.appendToNalUnit(dataArray, offset, limit);\n    suffixSei.appendToNalUnit(dataArray, offset, limit);\n  }\n\n  private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {\n    if (hasOutputFormat) {\n      sampleReader.endNalUnit(position, offset);\n    } else {\n      vps.endNalUnit(discardPadding);\n      sps.endNalUnit(discardPadding);\n      pps.endNalUnit(discardPadding);\n      if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {\n        output.format(parseMediaFormat(formatId, vps, sps, pps));\n        hasOutputFormat = true;\n      }\n    }\n    if (prefixSei.endNalUnit(discardPadding)) {\n      int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength);\n      seiWrapper.reset(prefixSei.nalData, unescapedLength);\n\n      // Skip the NAL prefix and type.\n      seiWrapper.skipBytes(5);\n      seiReader.consume(pesTimeUs, seiWrapper);\n    }\n    if (suffixSei.endNalUnit(discardPadding)) {\n      int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength);\n      seiWrapper.reset(suffixSei.nalData, unescapedLength);\n\n      // Skip the NAL prefix and type.\n      seiWrapper.skipBytes(5);\n      seiReader.consume(pesTimeUs, seiWrapper);\n    }\n  }\n\n  private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,\n      NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {\n    // Build codec-specific data.\n    byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];\n    System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);\n    System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength);\n    System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength);\n\n    // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1.\n    ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength);\n    bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id\n    int maxSubLayersMinus1 = bitArray.readBits(3);\n    bitArray.skipBit(); // sps_temporal_id_nesting_flag\n\n    // profile_tier_level(1, sps_max_sub_layers_minus1)\n    bitArray.skipBits(88); // if (profilePresentFlag) {...}\n    bitArray.skipBits(8); // general_level_idc\n    int toSkip = 0;\n    for (int i = 0; i < maxSubLayersMinus1; i++) {\n      if (bitArray.readBit()) { // sub_layer_profile_present_flag[i]\n        toSkip += 89;\n      }\n      if (bitArray.readBit()) { // sub_layer_level_present_flag[i]\n        toSkip += 8;\n      }\n    }\n    bitArray.skipBits(toSkip);\n    if (maxSubLayersMinus1 > 0) {\n      bitArray.skipBits(2 * (8 - maxSubLayersMinus1));\n    }\n\n    bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id\n    int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt();\n    if (chromaFormatIdc == 3) {\n      bitArray.skipBit(); // separate_colour_plane_flag\n    }\n    int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();\n    int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();\n    if (bitArray.readBit()) { // conformance_window_flag\n      int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt();\n      int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt();\n      int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt();\n      int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt();\n      // H.265/HEVC (2014) Table 6-1\n      int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1;\n      int subHeightC = chromaFormatIdc == 1 ? 2 : 1;\n      picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset);\n      picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset);\n    }\n    bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8\n    bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8\n    int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt();\n    // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...)\n    for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) {\n      bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i]\n      bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i]\n      bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i]\n    }\n    bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3\n    bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size\n    bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2\n    bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size\n    bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter\n    bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra\n    // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}}\n    boolean scalingListEnabled = bitArray.readBit();\n    if (scalingListEnabled && bitArray.readBit()) {\n      skipScalingList(bitArray);\n    }\n    bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1)\n    if (bitArray.readBit()) { // pcm_enabled_flag\n      // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4)\n      bitArray.skipBits(8);\n      bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3\n      bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size\n      bitArray.skipBit(); // pcm_loop_filter_disabled_flag\n    }\n    // Skips all short term reference picture sets.\n    skipShortTermRefPicSets(bitArray);\n    if (bitArray.readBit()) { // long_term_ref_pics_present_flag\n      // num_long_term_ref_pics_sps\n      for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) {\n        int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4;\n        // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i]\n        bitArray.skipBits(ltRefPicPocLsbSpsLength + 1);\n      }\n    }\n    bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag\n    float pixelWidthHeightRatio = 1;\n    if (bitArray.readBit()) { // vui_parameters_present_flag\n      if (bitArray.readBit()) { // aspect_ratio_info_present_flag\n        int aspectRatioIdc = bitArray.readBits(8);\n        if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {\n          int sarWidth = bitArray.readBits(16);\n          int sarHeight = bitArray.readBits(16);\n          if (sarWidth != 0 && sarHeight != 0) {\n            pixelWidthHeightRatio = (float) sarWidth / sarHeight;\n          }\n        } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {\n          pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];\n        } else {\n          Log.w(TAG, \"Unexpected aspect_ratio_idc value: \" + aspectRatioIdc);\n        }\n      }\n    }\n\n    return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,\n        Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE,\n        Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null);\n  }\n\n  /**\n   * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4.\n   */\n  private static void skipScalingList(ParsableNalUnitBitArray bitArray) {\n    for (int sizeId = 0; sizeId < 4; sizeId++) {\n      for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) {\n        if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId]\n          // scaling_list_pred_matrix_id_delta[sizeId][matrixId]\n          bitArray.readUnsignedExpGolombCodedInt();\n        } else {\n          int coefNum = Math.min(64, 1 << (4 + (sizeId << 1)));\n          if (sizeId > 1) {\n            // scaling_list_dc_coef_minus8[sizeId - 2][matrixId]\n            bitArray.readSignedExpGolombCodedInt();\n          }\n          for (int i = 0; i < coefNum; i++) {\n            bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of\n   * them. See H.265/HEVC (2014) 7.3.7.\n   */\n  private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) {\n    int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();\n    boolean interRefPicSetPredictionFlag = false;\n    int numNegativePics;\n    int numPositivePics;\n    // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous\n    // one, so we just keep track of that rather than storing the whole array.\n    // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.\n    int previousNumDeltaPocs = 0;\n    for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {\n      if (stRpsIdx != 0) {\n        interRefPicSetPredictionFlag = bitArray.readBit();\n      }\n      if (interRefPicSetPredictionFlag) {\n        bitArray.skipBit(); // delta_rps_sign\n        bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1\n        for (int j = 0; j <= previousNumDeltaPocs; j++) {\n          if (bitArray.readBit()) { // used_by_curr_pic_flag[j]\n            bitArray.skipBit(); // use_delta_flag[j]\n          }\n        }\n      } else {\n        numNegativePics = bitArray.readUnsignedExpGolombCodedInt();\n        numPositivePics = bitArray.readUnsignedExpGolombCodedInt();\n        previousNumDeltaPocs = numNegativePics + numPositivePics;\n        for (int i = 0; i < numNegativePics; i++) {\n          bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]\n          bitArray.skipBit(); // used_by_curr_pic_s0_flag[i]\n        }\n        for (int i = 0; i < numPositivePics; i++) {\n          bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]\n          bitArray.skipBit(); // used_by_curr_pic_s1_flag[i]\n        }\n      }\n    }\n  }\n\n  private static final class SampleReader {\n\n    /**\n     * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a\n     * slice_segment_layer_rbsp.\n     */\n    private static final int FIRST_SLICE_FLAG_OFFSET = 2;\n\n    private final TrackOutput output;\n\n    // Per NAL unit state. A sample consists of one or more NAL units.\n    private long nalUnitStartPosition;\n    private boolean nalUnitHasKeyframeData;\n    private int nalUnitBytesRead;\n    private long nalUnitTimeUs;\n    private boolean lookingForFirstSliceFlag;\n    private boolean isFirstSlice;\n    private boolean isFirstParameterSet;\n\n    // Per sample state that gets reset at the start of each sample.\n    private boolean readingSample;\n    private boolean writingParameterSets;\n    private long samplePosition;\n    private long sampleTimeUs;\n    private boolean sampleIsKeyframe;\n\n    public SampleReader(TrackOutput output) {\n      this.output = output;\n    }\n\n    public void reset() {\n      lookingForFirstSliceFlag = false;\n      isFirstSlice = false;\n      isFirstParameterSet = false;\n      readingSample = false;\n      writingParameterSets = false;\n    }\n\n    public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {\n      isFirstSlice = false;\n      isFirstParameterSet = false;\n      nalUnitTimeUs = pesTimeUs;\n      nalUnitBytesRead = 0;\n      nalUnitStartPosition = position;\n\n      if (nalUnitType >= VPS_NUT) {\n        if (!writingParameterSets && readingSample) {\n          // This is a non-VCL NAL unit, so flush the previous sample.\n          outputSample(offset);\n          readingSample = false;\n        }\n        if (nalUnitType <= PPS_NUT) {\n          // This sample will have parameter sets at the start.\n          isFirstParameterSet = !writingParameterSets;\n          writingParameterSets = true;\n        }\n      }\n\n      // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp.\n      nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT);\n      lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R;\n    }\n\n    public void readNalUnitData(byte[] data, int offset, int limit) {\n      if (lookingForFirstSliceFlag) {\n        int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead;\n        if (headerOffset < limit) {\n          isFirstSlice = (data[headerOffset] & 0x80) != 0;\n          lookingForFirstSliceFlag = false;\n        } else {\n          nalUnitBytesRead += limit - offset;\n        }\n      }\n    }\n\n    public void endNalUnit(long position, int offset) {\n      if (writingParameterSets && isFirstSlice) {\n        // This sample has parameter sets. Reset the key-frame flag based on the first slice.\n        sampleIsKeyframe = nalUnitHasKeyframeData;\n        writingParameterSets = false;\n      } else if (isFirstParameterSet || isFirstSlice) {\n        // This NAL unit is at the start of a new sample (access unit).\n        if (readingSample) {\n          // Output the sample ending before this NAL unit.\n          int nalUnitLength = (int) (position - nalUnitStartPosition);\n          outputSample(offset + nalUnitLength);\n        }\n        samplePosition = nalUnitStartPosition;\n        sampleTimeUs = nalUnitTimeUs;\n        readingSample = true;\n        sampleIsKeyframe = nalUnitHasKeyframeData;\n      }\n    }\n\n    private void outputSample(int offset) {\n      @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;\n      int size = (int) (nalUnitStartPosition - samplePosition);\n      output.sampleMetadata(sampleTimeUs, flags, size, offset, null);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;\nimport static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Parses ID3 data and extracts individual text information frames.\n */\npublic final class Id3Reader implements ElementaryStreamReader {\n\n  private static final String TAG = \"Id3Reader\";\n\n  private final ParsableByteArray id3Header;\n\n  private TrackOutput output;\n\n  // State that should be reset on seek.\n  private boolean writingSample;\n\n  // Per sample state that gets reset at the start of each sample.\n  private long sampleTimeUs;\n  private int sampleSize;\n  private int sampleBytesRead;\n\n  public Id3Reader() {\n    id3Header = new ParsableByteArray(ID3_HEADER_LENGTH);\n  }\n\n  @Override\n  public void seek() {\n    writingSample = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);\n    output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3,\n        null, Format.NO_VALUE, null));\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {\n      return;\n    }\n    writingSample = true;\n    sampleTimeUs = pesTimeUs;\n    sampleSize = 0;\n    sampleBytesRead = 0;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    if (!writingSample) {\n      return;\n    }\n    int bytesAvailable = data.bytesLeft();\n    if (sampleBytesRead < ID3_HEADER_LENGTH) {\n      // We're still reading the ID3 header.\n      int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead);\n      System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,\n          headerBytesAvailable);\n      if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) {\n        // We've finished reading the ID3 header. Extract the sample size.\n        id3Header.setPosition(0);\n        if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte()\n            || '3' != id3Header.readUnsignedByte()) {\n          Log.w(TAG, \"Discarding invalid ID3 tag\");\n          writingSample = false;\n          return;\n        }\n        id3Header.skipBytes(3); // version (2) + flags (1)\n        sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt();\n      }\n    }\n    // Write data to the output.\n    int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead);\n    output.sampleData(data, bytesToWrite);\n    sampleBytesRead += bytesToWrite;\n  }\n\n  @Override\n  public void packetFinished() {\n    if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {\n      return;\n    }\n    output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n    writingSample = false;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Collections;\n\n/**\n * Parses and extracts samples from an AAC/LATM elementary stream.\n */\npublic final class LatmReader implements ElementaryStreamReader {\n\n  private static final int STATE_FINDING_SYNC_1 = 0;\n  private static final int STATE_FINDING_SYNC_2 = 1;\n  private static final int STATE_READING_HEADER = 2;\n  private static final int STATE_READING_SAMPLE = 3;\n\n  private static final int INITIAL_BUFFER_SIZE = 1024;\n  private static final int SYNC_BYTE_FIRST = 0x56;\n  private static final int SYNC_BYTE_SECOND = 0xE0;\n\n  private final String language;\n  private final ParsableByteArray sampleDataBuffer;\n  private final ParsableBitArray sampleBitArray;\n\n  // Track output info.\n  private TrackOutput output;\n  private Format format;\n  private String formatId;\n\n  // Parser state info.\n  private int state;\n  private int bytesRead;\n  private int sampleSize;\n  private int secondHeaderByte;\n  private long timeUs;\n\n  // Container data.\n  private boolean streamMuxRead;\n  private int audioMuxVersionA;\n  private int numSubframes;\n  private int frameLengthType;\n  private boolean otherDataPresent;\n  private long otherDataLenBits;\n  private int sampleRateHz;\n  private long sampleDurationUs;\n  private int channelCount;\n\n  /**\n   * @param language Track language.\n   */\n  public LatmReader(@Nullable String language) {\n    this.language = language;\n    sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE);\n    sampleBitArray = new ParsableBitArray(sampleDataBuffer.data);\n  }\n\n  @Override\n  public void seek() {\n    state = STATE_FINDING_SYNC_1;\n    streamMuxRead = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);\n    formatId = idGenerator.getFormatId();\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) throws ParserException {\n    int bytesToRead;\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_SYNC_1:\n          if (data.readUnsignedByte() == SYNC_BYTE_FIRST) {\n            state = STATE_FINDING_SYNC_2;\n          }\n          break;\n        case STATE_FINDING_SYNC_2:\n          int secondByte = data.readUnsignedByte();\n          if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) {\n            secondHeaderByte = secondByte;\n            state = STATE_READING_HEADER;\n          } else if (secondByte != SYNC_BYTE_FIRST) {\n            state = STATE_FINDING_SYNC_1;\n          }\n          break;\n        case STATE_READING_HEADER:\n          sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte();\n          if (sampleSize > sampleDataBuffer.data.length) {\n            resetBufferForSize(sampleSize);\n          }\n          bytesRead = 0;\n          state = STATE_READING_SAMPLE;\n          break;\n        case STATE_READING_SAMPLE:\n          bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);\n          data.readBytes(sampleBitArray.data, bytesRead, bytesToRead);\n          bytesRead += bytesToRead;\n          if (bytesRead == sampleSize) {\n            sampleBitArray.setPosition(0);\n            parseAudioMuxElement(sampleBitArray);\n            state = STATE_FINDING_SYNC_1;\n          }\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41.\n   *\n   * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.\n   */\n  private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {\n    boolean useSameStreamMux = data.readBit();\n    if (!useSameStreamMux) {\n      streamMuxRead = true;\n      parseStreamMuxConfig(data);\n    } else if (!streamMuxRead) {\n      return; // Parsing cannot continue without StreamMuxConfig information.\n    }\n\n    if (audioMuxVersionA == 0) {\n      if (numSubframes != 0) {\n        throw new ParserException();\n      }\n      int muxSlotLengthBytes = parsePayloadLengthInfo(data);\n      parsePayloadMux(data, muxSlotLengthBytes);\n      if (otherDataPresent) {\n        data.skipBits((int) otherDataLenBits);\n      }\n    } else {\n      throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009.\n    }\n  }\n\n  /**\n   * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42.\n   */\n  private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {\n    int audioMuxVersion = data.readBits(1);\n    audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;\n    if (audioMuxVersionA == 0) {\n      if (audioMuxVersion == 1) {\n        latmGetValue(data); // Skip taraBufferFullness.\n      }\n      if (!data.readBit()) {\n        throw new ParserException();\n      }\n      numSubframes = data.readBits(6);\n      int numProgram = data.readBits(4);\n      int numLayer = data.readBits(3);\n      if (numProgram != 0 || numLayer != 0) {\n        throw new ParserException();\n      }\n      if (audioMuxVersion == 0) {\n        int startPosition = data.getPosition();\n        int readBits = parseAudioSpecificConfig(data);\n        data.setPosition(startPosition);\n        byte[] initData = new byte[(readBits + 7) / 8];\n        data.readBits(initData, 0, readBits);\n        Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,\n            Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz,\n            Collections.singletonList(initData), null, 0, language);\n        if (!format.equals(this.format)) {\n          this.format = format;\n          sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;\n          output.format(format);\n        }\n      } else {\n        int ascLen = (int) latmGetValue(data);\n        int bitsRead = parseAudioSpecificConfig(data);\n        data.skipBits(ascLen - bitsRead); // fillBits.\n      }\n      parseFrameLength(data);\n      otherDataPresent = data.readBit();\n      otherDataLenBits = 0;\n      if (otherDataPresent) {\n        if (audioMuxVersion == 1) {\n          otherDataLenBits = latmGetValue(data);\n        } else {\n          boolean otherDataLenEsc;\n          do {\n            otherDataLenEsc = data.readBit();\n            otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8);\n          } while (otherDataLenEsc);\n        }\n      }\n      boolean crcCheckPresent = data.readBit();\n      if (crcCheckPresent) {\n        data.skipBits(8); // crcCheckSum.\n      }\n    } else {\n      throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009.\n    }\n  }\n\n  private void parseFrameLength(ParsableBitArray data) {\n    frameLengthType = data.readBits(3);\n    switch (frameLengthType) {\n      case 0:\n        data.skipBits(8); // latmBufferFullness.\n        break;\n      case 1:\n        data.skipBits(9); // frameLength.\n        break;\n      case 3:\n      case 4:\n      case 5:\n        data.skipBits(6); // CELPframeLengthTableIndex.\n        break;\n      case 6:\n      case 7:\n        data.skipBits(1); // HVXCframeLengthTableIndex.\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException {\n    int bitsLeft = data.bitsLeft();\n    Pair<Integer, Integer> config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true);\n    sampleRateHz = config.first;\n    channelCount = config.second;\n    return bitsLeft - data.bitsLeft();\n  }\n\n  private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException {\n    int muxSlotLengthBytes = 0;\n    // Assuming single program and single layer.\n    if (frameLengthType == 0) {\n      int tmp;\n      do {\n        tmp = data.readBits(8);\n        muxSlotLengthBytes += tmp;\n      } while (tmp == 255);\n      return muxSlotLengthBytes;\n    } else {\n      throw new ParserException();\n    }\n  }\n\n  private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {\n    // The start of sample data in\n    int bitPosition = data.getPosition();\n    if ((bitPosition & 0x07) == 0) {\n      // Sample data is byte-aligned. We can output it directly.\n      sampleDataBuffer.setPosition(bitPosition >> 3);\n    } else {\n      // Sample data is not byte-aligned and we need align it ourselves before outputting.\n      // Byte alignment is needed because LATM framing is not supported by MediaCodec.\n      data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8);\n      sampleDataBuffer.setPosition(0);\n    }\n    output.sampleData(sampleDataBuffer, muxLengthBytes);\n    output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null);\n    timeUs += sampleDurationUs;\n  }\n\n  private void resetBufferForSize(int newSize) {\n    sampleDataBuffer.reset(newSize);\n    sampleBitArray.reset(sampleDataBuffer.data);\n  }\n\n  private static long latmGetValue(ParsableBitArray data) {\n    int bytesForValue = data.readBits(2);\n    return data.readBits((bytesForValue + 1) * 8);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Parses a continuous MPEG Audio byte stream and extracts individual frames.\n */\npublic final class MpegAudioReader implements ElementaryStreamReader {\n\n  private static final int STATE_FINDING_HEADER = 0;\n  private static final int STATE_READING_HEADER = 1;\n  private static final int STATE_READING_FRAME = 2;\n\n  private static final int HEADER_SIZE = 4;\n\n  private final ParsableByteArray headerScratch;\n  private final MpegAudioHeader header;\n  private final String language;\n\n  private String formatId;\n  private TrackOutput output;\n\n  private int state;\n  private int frameBytesRead;\n  private boolean hasOutputFormat;\n\n  // Used when finding the frame header.\n  private boolean lastByteWasFF;\n\n  // Parsed from the frame header.\n  private long frameDurationUs;\n  private int frameSize;\n\n  // The timestamp to attach to the next sample in the current packet.\n  private long timeUs;\n\n  public MpegAudioReader() {\n    this(null);\n  }\n\n  public MpegAudioReader(String language) {\n    state = STATE_FINDING_HEADER;\n    // The first byte of an MPEG Audio frame header is always 0xFF.\n    headerScratch = new ParsableByteArray(4);\n    headerScratch.data[0] = (byte) 0xFF;\n    header = new MpegAudioHeader();\n    this.language = language;\n  }\n\n  @Override\n  public void seek() {\n    state = STATE_FINDING_HEADER;\n    frameBytesRead = 0;\n    lastByteWasFF = false;\n  }\n\n  @Override\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    idGenerator.generateNewId();\n    formatId = idGenerator.getFormatId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);\n  }\n\n  @Override\n  public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {\n    timeUs = pesTimeUs;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data) {\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_HEADER:\n          findHeader(data);\n          break;\n        case STATE_READING_HEADER:\n          readHeaderRemainder(data);\n          break;\n        case STATE_READING_FRAME:\n          readFrameRemainder(data);\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  @Override\n  public void packetFinished() {\n    // Do nothing.\n  }\n\n  /**\n   * Attempts to locate the start of the next frame header.\n   * <p>\n   * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the\n   * first two bytes of the header are written into {@link #headerScratch}, and the position of the\n   * source is advanced to the byte that immediately follows these two bytes.\n   * <p>\n   * If a frame header is not located then the position of the source is advanced to the limit, and\n   * the method should be called again with the next source to continue the search.\n   *\n   * @param source The source from which to read.\n   */\n  private void findHeader(ParsableByteArray source) {\n    byte[] data = source.data;\n    int startOffset = source.getPosition();\n    int endOffset = source.limit();\n    for (int i = startOffset; i < endOffset; i++) {\n      boolean byteIsFF = (data[i] & 0xFF) == 0xFF;\n      boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0;\n      lastByteWasFF = byteIsFF;\n      if (found) {\n        source.setPosition(i + 1);\n        // Reset lastByteWasFF for next time.\n        lastByteWasFF = false;\n        headerScratch.data[1] = data[i];\n        frameBytesRead = 2;\n        state = STATE_READING_HEADER;\n        return;\n      }\n    }\n    source.setPosition(endOffset);\n  }\n\n  /**\n   * Attempts to read the remaining two bytes of the frame header.\n   * <p>\n   * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},\n   * the media format is output if this has not previously occurred, the four header bytes are\n   * output as sample data, and the position of the source is advanced to the byte that immediately\n   * follows the header.\n   * <p>\n   * If a frame header is read in full but cannot be parsed then the state is changed to\n   * {@link #STATE_READING_HEADER}.\n   * <p>\n   * If a frame header is not read in full then the position of the source is advanced to the limit,\n   * and the method should be called again with the next source to continue the read.\n   *\n   * @param source The source from which to read.\n   */\n  private void readHeaderRemainder(ParsableByteArray source) {\n    int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);\n    source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);\n    frameBytesRead += bytesToRead;\n    if (frameBytesRead < HEADER_SIZE) {\n      // We haven't read the whole header yet.\n      return;\n    }\n\n    headerScratch.setPosition(0);\n    boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header);\n    if (!parsedHeader) {\n      // We thought we'd located a frame header, but we hadn't.\n      frameBytesRead = 0;\n      state = STATE_READING_HEADER;\n      return;\n    }\n\n    frameSize = header.frameSize;\n    if (!hasOutputFormat) {\n      frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate;\n      Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null,\n          Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate,\n          null, null, 0, language);\n      output.format(format);\n      hasOutputFormat = true;\n    }\n\n    headerScratch.setPosition(0);\n    output.sampleData(headerScratch, HEADER_SIZE);\n    state = STATE_READING_FRAME;\n  }\n\n  /**\n   * Attempts to read the remainder of the frame.\n   * <p>\n   * If a frame is read in full then true is returned. The frame will have been output, and the\n   * position of the source will have been advanced to the byte that immediately follows the end of\n   * the frame.\n   * <p>\n   * If a frame is not read in full then the position of the source will have been advanced to the\n   * limit, and the method should be called again with the next source to continue the read.\n   *\n   * @param source The source from which to read.\n   */\n  private void readFrameRemainder(ParsableByteArray source) {\n    int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);\n    output.sampleData(source, bytesToRead);\n    frameBytesRead += bytesToRead;\n    if (frameBytesRead < frameSize) {\n      // We haven't read the whole of the frame yet.\n      return;\n    }\n\n    output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frameSize, 0, null);\n    timeUs += frameDurationUs;\n    frameBytesRead = 0;\n    state = STATE_FINDING_HEADER;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Arrays;\n\n/**\n * A buffer that fills itself with data corresponding to a specific NAL unit, as it is\n * encountered in the stream.\n */\n/* package */ final class NalUnitTargetBuffer {\n\n  private final int targetType;\n\n  private boolean isFilling;\n  private boolean isCompleted;\n\n  public byte[] nalData;\n  public int nalLength;\n\n  public NalUnitTargetBuffer(int targetType, int initialCapacity) {\n    this.targetType = targetType;\n\n    // Initialize data with a start code in the first three bytes.\n    nalData = new byte[3 + initialCapacity];\n    nalData[2] = 1;\n  }\n\n  /**\n   * Resets the buffer, clearing any data that it holds.\n   */\n  public void reset() {\n    isFilling = false;\n    isCompleted = false;\n  }\n\n  /**\n   * Returns whether the buffer currently holds a complete NAL unit of the target type.\n   */\n  public boolean isCompleted() {\n    return isCompleted;\n  }\n\n  /**\n   * Called to indicate that a NAL unit has started.\n   *\n   * @param type The type of the NAL unit.\n   */\n  public void startNalUnit(int type) {\n    Assertions.checkState(!isFilling);\n    isFilling = type == targetType;\n    if (isFilling) {\n      // Skip the three byte start code when writing data.\n      nalLength = 3;\n      isCompleted = false;\n    }\n  }\n\n  /**\n   * Called to pass stream data. The data passed should not include the 3 byte start code.\n   *\n   * @param data Holds the data being passed.\n   * @param offset The offset of the data in {@code data}.\n   * @param limit The limit (exclusive) of the data in {@code data}.\n   */\n  public void appendToNalUnit(byte[] data, int offset, int limit) {\n    if (!isFilling) {\n      return;\n    }\n    int readLength = limit - offset;\n    if (nalData.length < nalLength + readLength) {\n      nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2);\n    }\n    System.arraycopy(data, offset, nalData, nalLength, readLength);\n    nalLength += readLength;\n  }\n\n  /**\n   * Called to indicate that a NAL unit has ended.\n   *\n   * @param discardPadding The number of excess bytes that were passed to\n   *     {@link #appendToNalUnit(byte[], int, int)}, which should be discarded.\n   * @return Whether the ended NAL unit is of the target type.\n   */\n  public boolean endNalUnit(int discardPadding) {\n    if (!isFilling) {\n      return false;\n    }\n    nalLength -= discardPadding;\n    isFilling = false;\n    isCompleted = true;\n    return true;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\n\n/**\n * Parses PES packet data and extracts samples.\n */\npublic final class PesReader implements TsPayloadReader {\n\n  private static final String TAG = \"PesReader\";\n\n  private static final int STATE_FINDING_HEADER = 0;\n  private static final int STATE_READING_HEADER = 1;\n  private static final int STATE_READING_HEADER_EXTENSION = 2;\n  private static final int STATE_READING_BODY = 3;\n\n  private static final int HEADER_SIZE = 9;\n  private static final int MAX_HEADER_EXTENSION_SIZE = 10;\n  private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE)\n\n  private final ElementaryStreamReader reader;\n  private final ParsableBitArray pesScratch;\n\n  private int state;\n  private int bytesRead;\n\n  private TimestampAdjuster timestampAdjuster;\n  private boolean ptsFlag;\n  private boolean dtsFlag;\n  private boolean seenFirstDts;\n  private int extendedHeaderLength;\n  private int payloadSize;\n  private boolean dataAlignmentIndicator;\n  private long timeUs;\n\n  public PesReader(ElementaryStreamReader reader) {\n    this.reader = reader;\n    pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);\n    state = STATE_FINDING_HEADER;\n  }\n\n  @Override\n  public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n      TrackIdGenerator idGenerator) {\n    this.timestampAdjuster = timestampAdjuster;\n    reader.createTracks(extractorOutput, idGenerator);\n  }\n\n  // TsPayloadReader implementation.\n\n  @Override\n  public final void seek() {\n    state = STATE_FINDING_HEADER;\n    bytesRead = 0;\n    seenFirstDts = false;\n    reader.seek();\n  }\n\n  @Override\n  public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {\n    if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {\n      switch (state) {\n        case STATE_FINDING_HEADER:\n        case STATE_READING_HEADER:\n          // Expected.\n          break;\n        case STATE_READING_HEADER_EXTENSION:\n          Log.w(TAG, \"Unexpected start indicator reading extended header\");\n          break;\n        case STATE_READING_BODY:\n          // If payloadSize == -1 then the length of the previous packet was unspecified, and so\n          // we only know that it's finished now that we've seen the start of the next one. This\n          // is expected. If payloadSize != -1, then the length of the previous packet was known,\n          // but we didn't receive that amount of data. This is not expected.\n          if (payloadSize != -1) {\n            Log.w(TAG, \"Unexpected start indicator: expected \" + payloadSize + \" more bytes\");\n          }\n          // Either way, notify the reader that it has now finished.\n          reader.packetFinished();\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n      setState(STATE_READING_HEADER);\n    }\n\n    while (data.bytesLeft() > 0) {\n      switch (state) {\n        case STATE_FINDING_HEADER:\n          data.skipBytes(data.bytesLeft());\n          break;\n        case STATE_READING_HEADER:\n          if (continueRead(data, pesScratch.data, HEADER_SIZE)) {\n            setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);\n          }\n          break;\n        case STATE_READING_HEADER_EXTENSION:\n          int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);\n          // Read as much of the extended header as we're interested in, and skip the rest.\n          if (continueRead(data, pesScratch.data, readLength)\n              && continueRead(data, null, extendedHeaderLength)) {\n            parseHeaderExtension();\n            flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;\n            reader.packetStarted(timeUs, flags);\n            setState(STATE_READING_BODY);\n          }\n          break;\n        case STATE_READING_BODY:\n          readLength = data.bytesLeft();\n          int padding = payloadSize == -1 ? 0 : readLength - payloadSize;\n          if (padding > 0) {\n            readLength -= padding;\n            data.setLimit(data.getPosition() + readLength);\n          }\n          reader.consume(data);\n          if (payloadSize != -1) {\n            payloadSize -= readLength;\n            if (payloadSize == 0) {\n              reader.packetFinished();\n              setState(STATE_READING_HEADER);\n            }\n          }\n          break;\n        default:\n          throw new IllegalStateException();\n      }\n    }\n  }\n\n  private void setState(int state) {\n    this.state = state;\n    bytesRead = 0;\n  }\n\n  /**\n   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed\n   * that the data should be written into {@code target} starting from an offset of zero.\n   *\n   * @param source The source from which to read.\n   * @param target The target into which data is to be read, or {@code null} to skip.\n   * @param targetLength The target length of the read.\n   * @return Whether the target length has been reached.\n   */\n  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {\n    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);\n    if (bytesToRead <= 0) {\n      return true;\n    } else if (target == null) {\n      source.skipBytes(bytesToRead);\n    } else {\n      source.readBytes(target, bytesRead, bytesToRead);\n    }\n    bytesRead += bytesToRead;\n    return bytesRead == targetLength;\n  }\n\n  private boolean parseHeader() {\n    // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of\n    // the header.\n    pesScratch.setPosition(0);\n    int startCodePrefix = pesScratch.readBits(24);\n    if (startCodePrefix != 0x000001) {\n      Log.w(TAG, \"Unexpected start code prefix: \" + startCodePrefix);\n      payloadSize = -1;\n      return false;\n    }\n\n    pesScratch.skipBits(8); // stream_id.\n    int packetLength = pesScratch.readBits(16);\n    pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)\n    dataAlignmentIndicator = pesScratch.readBit();\n    pesScratch.skipBits(2); // copyright (1), original_or_copy (1)\n    ptsFlag = pesScratch.readBit();\n    dtsFlag = pesScratch.readBit();\n    // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),\n    // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)\n    pesScratch.skipBits(6);\n    extendedHeaderLength = pesScratch.readBits(8);\n\n    if (packetLength == 0) {\n      payloadSize = -1;\n    } else {\n      payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */\n          - HEADER_SIZE - extendedHeaderLength;\n    }\n    return true;\n  }\n\n  private void parseHeaderExtension() {\n    pesScratch.setPosition(0);\n    timeUs = C.TIME_UNSET;\n    if (ptsFlag) {\n      pesScratch.skipBits(4); // '0010' or '0011'\n      long pts = (long) pesScratch.readBits(3) << 30;\n      pesScratch.skipBits(1); // marker_bit\n      pts |= pesScratch.readBits(15) << 15;\n      pesScratch.skipBits(1); // marker_bit\n      pts |= pesScratch.readBits(15);\n      pesScratch.skipBits(1); // marker_bit\n      if (!seenFirstDts && dtsFlag) {\n        pesScratch.skipBits(4); // '0011'\n        long dts = (long) pesScratch.readBits(3) << 30;\n        pesScratch.skipBits(1); // marker_bit\n        dts |= pesScratch.readBits(15) << 15;\n        pesScratch.skipBits(1); // marker_bit\n        dts |= pesScratch.readBits(15);\n        pesScratch.skipBits(1); // marker_bit\n        // Subsequent PES packets may have earlier presentation timestamps than this one, but they\n        // should all be greater than or equal to this packet's decode timestamp. We feed the\n        // decode timestamp to the adjuster here so that in the case that this is the first to be\n        // fed, the adjuster will be able to compute an offset to apply such that the adjusted\n        // presentation timestamps of all future packets are non-negative.\n        timestampAdjuster.adjustTsTimestamp(dts);\n        seenFirstDts = true;\n      }\n      timeUs = timestampAdjuster.adjustTsTimestamp(pts);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.BinarySearchSeeker;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A seeker that supports seeking within PS stream using binary search.\n *\n * <p>This seeker uses the first and last SCR values within the stream, as well as the stream\n * duration to interpolate the SCR value of the seeking position. Then it performs binary search\n * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from\n * the target SCR.\n */\n/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {\n\n  private static final long SEEK_TOLERANCE_US = 100_000;\n  private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;\n  private static final int TIMESTAMP_SEARCH_BYTES = 20000;\n\n  public PsBinarySearchSeeker(\n      TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {\n    super(\n        new DefaultSeekTimestampConverter(),\n        new PsScrSeeker(scrTimestampAdjuster),\n        streamDurationUs,\n        /* floorTimePosition= */ 0,\n        /* ceilingTimePosition= */ streamDurationUs + 1,\n        /* floorBytePosition= */ 0,\n        /* ceilingBytePosition= */ inputLength,\n        /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,\n        MINIMUM_SEARCH_RANGE_BYTES);\n  }\n\n  /**\n   * A seeker that looks for a given SCR timestamp at a given position in a PS stream.\n   *\n   * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link\n   * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and\n   * then compare the SCR timestamps (if available) of these packets to the target timestamp.\n   */\n  private static final class PsScrSeeker implements TimestampSeeker {\n\n    private final TimestampAdjuster scrTimestampAdjuster;\n    private final ParsableByteArray packetBuffer;\n\n    private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {\n      this.scrTimestampAdjuster = scrTimestampAdjuster;\n      packetBuffer = new ParsableByteArray();\n    }\n\n    @Override\n    public TimestampSearchResult searchForTimestamp(\n        ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)\n        throws IOException, InterruptedException {\n      long inputPosition = input.getPosition();\n      int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);\n\n      packetBuffer.reset(bytesToSearch);\n      input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n      return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);\n    }\n\n    @Override\n    public void onSeekFinished() {\n      packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);\n    }\n\n    private TimestampSearchResult searchForScrValueInBuffer(\n        ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {\n      int startOfLastPacketPosition = C.POSITION_UNSET;\n      int endOfLastPacketPosition = C.POSITION_UNSET;\n      long lastScrTimeUsInRange = C.TIME_UNSET;\n\n      while (packetBuffer.bytesLeft() >= 4) {\n        int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());\n        if (nextStartCode != PsExtractor.PACK_START_CODE) {\n          packetBuffer.skipBytes(1);\n          continue;\n        } else {\n          packetBuffer.skipBytes(4);\n        }\n\n        // We found a pack.\n        long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);\n        if (scrValue != C.TIME_UNSET) {\n          long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);\n          if (scrTimeUs > targetScrTimeUs) {\n            if (lastScrTimeUsInRange == C.TIME_UNSET) {\n              // First SCR timestamp is already over target.\n              return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);\n            } else {\n              // Last SCR timestamp < target timestamp < this timestamp.\n              return TimestampSearchResult.targetFoundResult(\n                  bufferStartOffset + startOfLastPacketPosition);\n            }\n          } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {\n            long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();\n            return TimestampSearchResult.targetFoundResult(startOfPacketInStream);\n          }\n\n          lastScrTimeUsInRange = scrTimeUs;\n          startOfLastPacketPosition = packetBuffer.getPosition();\n        }\n        skipToEndOfCurrentPack(packetBuffer);\n        endOfLastPacketPosition = packetBuffer.getPosition();\n      }\n\n      if (lastScrTimeUsInRange != C.TIME_UNSET) {\n        long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;\n        return TimestampSearchResult.underestimatedResult(\n            lastScrTimeUsInRange, endOfLastPacketPositionInStream);\n      } else {\n        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;\n      }\n    }\n\n    /**\n     * Skips the buffer position to the position after the end of the current PS pack in the buffer,\n     * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in\n     * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.\n     */\n    private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {\n      int limit = packetBuffer.limit();\n\n      if (packetBuffer.bytesLeft() < 10) {\n        // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing\n        // length.\n        packetBuffer.setPosition(limit);\n        return;\n      }\n      packetBuffer.skipBytes(9);\n\n      int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;\n      if (packetBuffer.bytesLeft() < packStuffingLength) {\n        packetBuffer.setPosition(limit);\n        return;\n      }\n      packetBuffer.skipBytes(packStuffingLength);\n\n      if (packetBuffer.bytesLeft() < 4) {\n        packetBuffer.setPosition(limit);\n        return;\n      }\n\n      int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());\n      if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {\n        packetBuffer.skipBytes(4);\n        int systemHeaderLength = packetBuffer.readUnsignedShort();\n        if (packetBuffer.bytesLeft() < systemHeaderLength) {\n          packetBuffer.setPosition(limit);\n          return;\n        }\n        packetBuffer.skipBytes(systemHeaderLength);\n      }\n\n      // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right\n      // after the end position of this pack.\n      // If we couldn't find these codes within the buffer, return the buffer limit, or return\n      // the first position which PES packets pattern does not match (some malformed packets).\n      while (packetBuffer.bytesLeft() >= 4) {\n        nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());\n        if (nextStartCode == PsExtractor.PACK_START_CODE\n            || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {\n          break;\n        }\n        if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {\n          break;\n        }\n        packetBuffer.skipBytes(4);\n\n        if (packetBuffer.bytesLeft() < 2) {\n          // 2 bytes for PES_packet length.\n          packetBuffer.setPosition(limit);\n          return;\n        }\n        int pesPacketLength = packetBuffer.readUnsignedShort();\n        packetBuffer.setPosition(\n            Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));\n      }\n    }\n  }\n\n  private static int peekIntAtPosition(byte[] data, int position) {\n    return (data[position] & 0xFF) << 24\n        | (data[position + 1] & 0xFF) << 16\n        | (data[position + 2] & 0xFF) << 8\n        | (data[position + 3] & 0xFF);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A reader that can extract the approximate duration from a given MPEG program stream (PS).\n *\n * <p>This reader extracts the duration by reading system clock reference (SCR) values from the\n * header of a pack at the start and at the end of the stream, calculating the difference, and\n * converting that into stream duration. This reader also handles the case when a single SCR\n * wraparound takes place within the stream, which can make SCR values at the beginning of the\n * stream larger than SCR values at the end. This class can only be used once to read duration from\n * a given stream, and the usage of the class is not thread-safe, so all calls should be made from\n * the same thread.\n *\n * <p>Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header.\n */\n/* package */ final class PsDurationReader {\n\n  private static final int TIMESTAMP_SEARCH_BYTES = 20000;\n\n  private final TimestampAdjuster scrTimestampAdjuster;\n  private final ParsableByteArray packetBuffer;\n\n  private boolean isDurationRead;\n  private boolean isFirstScrValueRead;\n  private boolean isLastScrValueRead;\n\n  private long firstScrValue;\n  private long lastScrValue;\n  private long durationUs;\n\n  /* package */ PsDurationReader() {\n    scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);\n    firstScrValue = C.TIME_UNSET;\n    lastScrValue = C.TIME_UNSET;\n    durationUs = C.TIME_UNSET;\n    packetBuffer = new ParsableByteArray();\n  }\n\n  /** Returns true if a PS duration has been read. */\n  public boolean isDurationReadFinished() {\n    return isDurationRead;\n  }\n\n  public TimestampAdjuster getScrTimestampAdjuster() {\n    return scrTimestampAdjuster;\n  }\n\n  /**\n   * Reads a PS duration from the input.\n   *\n   * <p>This reader reads the duration by reading SCR values from the header of a pack at the start\n   * and at the end of the stream, calculating the difference, and converting that into stream\n   * duration.\n   *\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated\n   *     to hold the position of the required seek.\n   * @return One of the {@code RESULT_} values defined in {@link Extractor}.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public @Extractor.ReadResult int readDuration(\n      ExtractorInput input, PositionHolder seekPositionHolder)\n      throws IOException, InterruptedException {\n    if (!isLastScrValueRead) {\n      return readLastScrValue(input, seekPositionHolder);\n    }\n    if (lastScrValue == C.TIME_UNSET) {\n      return finishReadDuration(input);\n    }\n    if (!isFirstScrValueRead) {\n      return readFirstScrValue(input, seekPositionHolder);\n    }\n    if (firstScrValue == C.TIME_UNSET) {\n      return finishReadDuration(input);\n    }\n\n    long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue);\n    long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue);\n    durationUs = maxScrPositionUs - minScrPositionUs;\n    return finishReadDuration(input);\n  }\n\n  /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  /**\n   * Returns the SCR value read from the next pack in the stream, given the buffer at the pack\n   * header start position (just behind the pack start code).\n   */\n  public static long readScrValueFromPack(ParsableByteArray packetBuffer) {\n    int originalPosition = packetBuffer.getPosition();\n    if (packetBuffer.bytesLeft() < 9) {\n      // We require at 9 bytes for pack header to read scr value\n      return C.TIME_UNSET;\n    }\n    byte[] scrBytes = new byte[9];\n    packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length);\n    packetBuffer.setPosition(originalPosition);\n    if (!checkMarkerBits(scrBytes)) {\n      return C.TIME_UNSET;\n    }\n    return readScrValueFromPackHeader(scrBytes);\n  }\n\n  private int finishReadDuration(ExtractorInput input) {\n    packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);\n    isDurationRead = true;\n    input.resetPeekPosition();\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)\n      throws IOException, InterruptedException {\n    int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());\n    int searchStartPosition = 0;\n    if (input.getPosition() != searchStartPosition) {\n      seekPositionHolder.position = searchStartPosition;\n      return Extractor.RESULT_SEEK;\n    }\n\n    packetBuffer.reset(bytesToSearch);\n    input.resetPeekPosition();\n    input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n    firstScrValue = readFirstScrValueFromBuffer(packetBuffer);\n    isFirstScrValueRead = true;\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) {\n    int searchStartPosition = packetBuffer.getPosition();\n    int searchEndPosition = packetBuffer.limit();\n    for (int searchPosition = searchStartPosition;\n        searchPosition < searchEndPosition - 3;\n        searchPosition++) {\n      int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);\n      if (nextStartCode == PsExtractor.PACK_START_CODE) {\n        packetBuffer.setPosition(searchPosition + 4);\n        long scrValue = readScrValueFromPack(packetBuffer);\n        if (scrValue != C.TIME_UNSET) {\n          return scrValue;\n        }\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n  private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)\n      throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);\n    long searchStartPosition = inputLength - bytesToSearch;\n    if (input.getPosition() != searchStartPosition) {\n      seekPositionHolder.position = searchStartPosition;\n      return Extractor.RESULT_SEEK;\n    }\n\n    packetBuffer.reset(bytesToSearch);\n    input.resetPeekPosition();\n    input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n    lastScrValue = readLastScrValueFromBuffer(packetBuffer);\n    isLastScrValueRead = true;\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {\n    int searchStartPosition = packetBuffer.getPosition();\n    int searchEndPosition = packetBuffer.limit();\n    for (int searchPosition = searchEndPosition - 4;\n        searchPosition >= searchStartPosition;\n        searchPosition--) {\n      int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);\n      if (nextStartCode == PsExtractor.PACK_START_CODE) {\n        packetBuffer.setPosition(searchPosition + 4);\n        long scrValue = readScrValueFromPack(packetBuffer);\n        if (scrValue != C.TIME_UNSET) {\n          return scrValue;\n        }\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n  private int peekIntAtPosition(byte[] data, int position) {\n    return (data[position] & 0xFF) << 24\n        | (data[position + 1] & 0xFF) << 16\n        | (data[position + 2] & 0xFF) << 8\n        | (data[position + 3] & 0xFF);\n  }\n\n  private static boolean checkMarkerBits(byte[] scrBytes) {\n    // Verify the 01xxx1xx marker on the 0th byte\n    if ((scrBytes[0] & 0xC4) != 0x44) {\n      return false;\n    }\n    // 1st byte belongs to scr field.\n    // Verify the xxxxx1xx marker on the 2nd byte\n    if ((scrBytes[2] & 0x04) != 0x04) {\n      return false;\n    }\n    // 3rd byte belongs to scr field.\n    // Verify the xxxxx1xx marker on the 4rd byte\n    if ((scrBytes[4] & 0x04) != 0x04) {\n      return false;\n    }\n    // Verify the xxxxxxx1 marker on the 5th byte\n    if ((scrBytes[5] & 0x01) != 0x01) {\n      return false;\n    }\n    // 6th and 7th bytes belongs to program_max_rate field.\n    // Verify the xxxxxx11 marker on the 8th byte\n    return (scrBytes[8] & 0x03) == 0x03;\n  }\n\n  /**\n   * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring\n   * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in\n   * pack_header.\n   *\n   * <p>We ignore SCR Ext, because it's too small to have any significance.\n   */\n  private static long readScrValueFromPackHeader(byte[] scrBytes) {\n    return ((scrBytes[0] & 0b00111000L) >> 3) << 30\n        | (scrBytes[0] & 0b00000011L) << 28\n        | (scrBytes[1] & 0xFFL) << 20\n        | ((scrBytes[2] & 0b11111000L) >> 3) << 15\n        | (scrBytes[2] & 0b00000011L) << 13\n        | (scrBytes[3] & 0xFFL) << 5\n        | (scrBytes[4] & 0b11111000L) >> 3;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.SparseArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.IOException;\n\n/**\n * Extracts data from the MPEG-2 PS container format.\n */\npublic final class PsExtractor implements Extractor {\n\n  /** Factory for {@link PsExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()};\n\n  /* package */ static final int PACK_START_CODE = 0x000001BA;\n  /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB;\n  /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001;\n  /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9;\n  private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;\n\n  // Max search length for first audio and video track in input data.\n  private static final long MAX_SEARCH_LENGTH = 1024 * 1024;\n  // Max search length for additional audio and video tracks in input data after at least one audio\n  // and video track has been found.\n  private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;\n\n  public static final int PRIVATE_STREAM_1 = 0xBD;\n  public static final int AUDIO_STREAM = 0xC0;\n  public static final int AUDIO_STREAM_MASK = 0xE0;\n  public static final int VIDEO_STREAM = 0xE0;\n  public static final int VIDEO_STREAM_MASK = 0xF0;\n\n  private final TimestampAdjuster timestampAdjuster;\n  private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid\n  private final ParsableByteArray psPacketBuffer;\n  private final PsDurationReader durationReader;\n\n  private boolean foundAllTracks;\n  private boolean foundAudioTrack;\n  private boolean foundVideoTrack;\n  private long lastTrackPosition;\n\n  // Accessed only by the loading thread.\n  private PsBinarySearchSeeker psBinarySearchSeeker;\n  private ExtractorOutput output;\n  private boolean hasOutputSeekMap;\n\n  public PsExtractor() {\n    this(new TimestampAdjuster(0));\n  }\n\n  public PsExtractor(TimestampAdjuster timestampAdjuster) {\n    this.timestampAdjuster = timestampAdjuster;\n    psPacketBuffer = new ParsableByteArray(4096);\n    psPayloadReaders = new SparseArray<>();\n    durationReader = new PsDurationReader();\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    byte[] scratch = new byte[14];\n    input.peekFully(scratch, 0, 14);\n\n    // Verify the PACK_START_CODE for the first 4 bytes\n    if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16)\n        | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) {\n      return false;\n    }\n    // Verify the 01xxx1xx marker on the 5th byte\n    if ((scratch[4] & 0xC4) != 0x44) {\n      return false;\n    }\n    // Verify the xxxxx1xx marker on the 7th byte\n    if ((scratch[6] & 0x04) != 0x04) {\n      return false;\n    }\n    // Verify the xxxxx1xx marker on the 9th byte\n    if ((scratch[8] & 0x04) != 0x04) {\n      return false;\n    }\n    // Verify the xxxxxxx1 marker on the 10th byte\n    if ((scratch[9] & 0x01) != 0x01) {\n      return false;\n    }\n    // Verify the xxxxxx11 marker on the 13th byte\n    if ((scratch[12] & 0x03) != 0x03) {\n      return false;\n    }\n    // Read the stuffing length from the 14th byte (last 3 bits)\n    int packStuffingLength = scratch[13] & 0x07;\n    input.advancePeekPosition(packStuffingLength);\n    // Now check that the next 3 bytes are the beginning of an MPEG start code\n    input.peekFully(scratch, 0, 3);\n    return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8)\n        | (scratch[2] & 0xFF)));\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.output = output;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    boolean hasNotEncounteredFirstTimestamp =\n        timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;\n    if (hasNotEncounteredFirstTimestamp\n        || (timestampAdjuster.getFirstSampleTimestampUs() != 0\n            && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {\n      // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to\n      // treat the first timestamp encountered as sample time 0, which is incorrect. In this case,\n      // we have to set the first sample timestamp manually.\n      // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a\n      // different position, we need to set the first sample timestamp manually again.\n      timestampAdjuster.reset();\n      timestampAdjuster.setFirstSampleTimestampUs(timeUs);\n    }\n\n    if (psBinarySearchSeeker != null) {\n      psBinarySearchSeeker.setSeekTargetUs(timeUs);\n    }\n    for (int i = 0; i < psPayloadReaders.size(); i++) {\n      psPayloadReaders.valueAt(i).seek();\n    }\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n\n    long inputLength = input.getLength();\n    boolean canReadDuration = inputLength != C.LENGTH_UNSET;\n    if (canReadDuration && !durationReader.isDurationReadFinished()) {\n      return durationReader.readDuration(input, seekPosition);\n    }\n    maybeOutputSeekMap(inputLength);\n    if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) {\n      return psBinarySearchSeeker.handlePendingSeek(\n          input, seekPosition, /* outputFrameHolder= */ null);\n    }\n\n    input.resetPeekPosition();\n    long peekBytesLeft =\n        inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET;\n    if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) {\n      return RESULT_END_OF_INPUT;\n    }\n    // First peek and check what type of start code is next.\n    if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {\n      return RESULT_END_OF_INPUT;\n    }\n\n    psPacketBuffer.setPosition(0);\n    int nextStartCode = psPacketBuffer.readInt();\n    if (nextStartCode == MPEG_PROGRAM_END_CODE) {\n      return RESULT_END_OF_INPUT;\n    } else if (nextStartCode == PACK_START_CODE) {\n      // Now peek the rest of the pack_header.\n      input.peekFully(psPacketBuffer.data, 0, 10);\n\n      // We only care about the pack_stuffing_length in here, skip the first 77 bits.\n      psPacketBuffer.setPosition(9);\n\n      // Last 3 bits is the length.\n      int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07;\n\n      // Now skip the stuffing and the pack header.\n      input.skipFully(packStuffingLength + 14);\n      return RESULT_CONTINUE;\n    } else if (nextStartCode == SYSTEM_HEADER_START_CODE) {\n      // We just skip all this, but we need to get the length first.\n      input.peekFully(psPacketBuffer.data, 0, 2);\n\n      // Length is the next 2 bytes.\n      psPacketBuffer.setPosition(0);\n      int systemHeaderLength = psPacketBuffer.readUnsignedShort();\n      input.skipFully(systemHeaderLength + 6);\n      return RESULT_CONTINUE;\n    } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) {\n      input.skipFully(1);  // Skip bytes until we see a valid start code again.\n      return RESULT_CONTINUE;\n    }\n\n    // We're at the start of a regular PES packet now.\n    // Get the stream ID off the last byte of the start code.\n    int streamId = nextStartCode & 0xFF;\n\n    // Check to see if we have this one in our map yet, and if not, then add it.\n    PesReader payloadReader = psPayloadReaders.get(streamId);\n    if (!foundAllTracks) {\n      if (payloadReader == null) {\n        ElementaryStreamReader elementaryStreamReader = null;\n        if (streamId == PRIVATE_STREAM_1) {\n          // Private stream, used for AC3 audio.\n          // NOTE: This may need further parsing to determine if its DTS, but that's likely only\n          // valid for DVDs.\n          elementaryStreamReader = new Ac3Reader();\n          foundAudioTrack = true;\n          lastTrackPosition = input.getPosition();\n        } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {\n          elementaryStreamReader = new MpegAudioReader();\n          foundAudioTrack = true;\n          lastTrackPosition = input.getPosition();\n        } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {\n          elementaryStreamReader = new H262Reader();\n          foundVideoTrack = true;\n          lastTrackPosition = input.getPosition();\n        }\n        if (elementaryStreamReader != null) {\n          TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);\n          elementaryStreamReader.createTracks(output, idGenerator);\n          payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);\n          psPayloadReaders.put(streamId, payloadReader);\n        }\n      }\n      long maxSearchPosition =\n          foundAudioTrack && foundVideoTrack\n              ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND\n              : MAX_SEARCH_LENGTH;\n      if (input.getPosition() > maxSearchPosition) {\n        foundAllTracks = true;\n        output.endTracks();\n      }\n    }\n\n    // The next 2 bytes are the length. Once we have that we can consume the complete packet.\n    input.peekFully(psPacketBuffer.data, 0, 2);\n    psPacketBuffer.setPosition(0);\n    int payloadLength = psPacketBuffer.readUnsignedShort();\n    int pesLength = payloadLength + 6;\n\n    if (payloadReader == null) {\n      // Just skip this data.\n      input.skipFully(pesLength);\n    } else {\n      psPacketBuffer.reset(pesLength);\n      // Read the whole packet and the header for consumption.\n      input.readFully(psPacketBuffer.data, 0, pesLength);\n      psPacketBuffer.setPosition(6);\n      payloadReader.consume(psPacketBuffer);\n      psPacketBuffer.setLimit(psPacketBuffer.capacity());\n    }\n\n    return RESULT_CONTINUE;\n  }\n\n  // Internals.\n\n  private void maybeOutputSeekMap(long inputLength) {\n    if (!hasOutputSeekMap) {\n      hasOutputSeekMap = true;\n      if (durationReader.getDurationUs() != C.TIME_UNSET) {\n        psBinarySearchSeeker =\n            new PsBinarySearchSeeker(\n                durationReader.getScrTimestampAdjuster(),\n                durationReader.getDurationUs(),\n                inputLength);\n        output.seekMap(psBinarySearchSeeker.getSeekMap());\n      } else {\n        output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));\n      }\n    }\n  }\n\n  /**\n   * Parses PES packet data and extracts samples.\n   */\n  private static final class PesReader {\n\n    private static final int PES_SCRATCH_SIZE = 64;\n\n    private final ElementaryStreamReader pesPayloadReader;\n    private final TimestampAdjuster timestampAdjuster;\n    private final ParsableBitArray pesScratch;\n\n    private boolean ptsFlag;\n    private boolean dtsFlag;\n    private boolean seenFirstDts;\n    private int extendedHeaderLength;\n    private long timeUs;\n\n    public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) {\n      this.pesPayloadReader = pesPayloadReader;\n      this.timestampAdjuster = timestampAdjuster;\n      pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);\n    }\n\n    /**\n     * Notifies the reader that a seek has occurred.\n     * <p>\n     * Following a call to this method, the data passed to the next invocation of\n     * {@link #consume(ParsableByteArray)} will not be a continuation of the data that was\n     * previously passed. Hence the reader should reset any internal state.\n     */\n    public void seek() {\n      seenFirstDts = false;\n      pesPayloadReader.seek();\n    }\n\n    /**\n     * Consumes the payload of a PS packet.\n     *\n     * @param data The PES packet. The position will be set to the start of the payload.\n     * @throws ParserException If the payload could not be parsed.\n     */\n    public void consume(ParsableByteArray data) throws ParserException {\n      data.readBytes(pesScratch.data, 0, 3);\n      pesScratch.setPosition(0);\n      parseHeader();\n      data.readBytes(pesScratch.data, 0, extendedHeaderLength);\n      pesScratch.setPosition(0);\n      parseHeaderExtension();\n      pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);\n      pesPayloadReader.consume(data);\n      // We always have complete PES packets with program stream.\n      pesPayloadReader.packetFinished();\n    }\n\n    private void parseHeader() {\n      // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of\n      // the header.\n      // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1),\n      // data_alignment_indicator (1), copyright (1), original_or_copy (1)\n      pesScratch.skipBits(8);\n      ptsFlag = pesScratch.readBit();\n      dtsFlag = pesScratch.readBit();\n      // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),\n      // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)\n      pesScratch.skipBits(6);\n      extendedHeaderLength = pesScratch.readBits(8);\n    }\n\n    private void parseHeaderExtension() {\n      timeUs = 0;\n      if (ptsFlag) {\n        pesScratch.skipBits(4); // '0010' or '0011'\n        long pts = (long) pesScratch.readBits(3) << 30;\n        pesScratch.skipBits(1); // marker_bit\n        pts |= pesScratch.readBits(15) << 15;\n        pesScratch.skipBits(1); // marker_bit\n        pts |= pesScratch.readBits(15);\n        pesScratch.skipBits(1); // marker_bit\n        if (!seenFirstDts && dtsFlag) {\n          pesScratch.skipBits(4); // '0011'\n          long dts = (long) pesScratch.readBits(3) << 30;\n          pesScratch.skipBits(1); // marker_bit\n          dts |= pesScratch.readBits(15) << 15;\n          pesScratch.skipBits(1); // marker_bit\n          dts |= pesScratch.readBits(15);\n          pesScratch.skipBits(1); // marker_bit\n          // Subsequent PES packets may have earlier presentation timestamps than this one, but they\n          // should all be greater than or equal to this packet's decode timestamp. We feed the\n          // decode timestamp to the adjuster here so that in the case that this is the first to be\n          // fed, the adjuster will be able to compute an offset to apply such that the adjusted\n          // presentation timestamps of all future packets are non-negative.\n          timestampAdjuster.adjustTsTimestamp(dts);\n          seenFirstDts = true;\n        }\n        timeUs = timestampAdjuster.adjustTsTimestamp(pts);\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\n\n/**\n * Reads section data.\n */\npublic interface SectionPayloadReader {\n\n  /**\n   * Initializes the section payload reader.\n   *\n   * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.\n   * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.\n   * @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the\n   *     {@link TrackOutput}s.\n   */\n  void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n            TrackIdGenerator idGenerator);\n\n  /**\n   * Called by a {@link SectionReader} when a full section is received.\n   *\n   * @param sectionData The data belonging to a section starting from the table_id. If\n   *     section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field.\n   *     Otherwise, all bytes belonging to the table section are included.\n   */\n  void consume(ParsableByteArray sectionData);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}.\n * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4.\n */\npublic final class SectionReader implements TsPayloadReader {\n\n  private static final int SECTION_HEADER_LENGTH = 3;\n  private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32;\n  private static final int MAX_SECTION_LENGTH = 4098;\n\n  private final SectionPayloadReader reader;\n  private final ParsableByteArray sectionData;\n\n  private int totalSectionLength;\n  private int bytesRead;\n  private boolean sectionSyntaxIndicator;\n  private boolean waitingForPayloadStart;\n\n  public SectionReader(SectionPayloadReader reader) {\n    this.reader = reader;\n    sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH);\n  }\n\n  @Override\n  public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n      TrackIdGenerator idGenerator) {\n    reader.init(timestampAdjuster, extractorOutput, idGenerator);\n    waitingForPayloadStart = true;\n  }\n\n  @Override\n  public void seek() {\n    waitingForPayloadStart = true;\n  }\n\n  @Override\n  public void consume(ParsableByteArray data, @Flags int flags) {\n    boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;\n    int payloadStartPosition = C.POSITION_UNSET;\n    if (payloadUnitStartIndicator) {\n      int payloadStartOffset = data.readUnsignedByte();\n      payloadStartPosition = data.getPosition() + payloadStartOffset;\n    }\n\n    if (waitingForPayloadStart) {\n      if (!payloadUnitStartIndicator) {\n        return;\n      }\n      waitingForPayloadStart = false;\n      data.setPosition(payloadStartPosition);\n      bytesRead = 0;\n    }\n\n    while (data.bytesLeft() > 0) {\n      if (bytesRead < SECTION_HEADER_LENGTH) {\n        // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of\n        // the header.\n        if (bytesRead == 0) {\n          int tableId = data.readUnsignedByte();\n          data.setPosition(data.getPosition() - 1);\n          if (tableId == 0xFF /* forbidden value */) {\n            // No more sections in this ts packet.\n            waitingForPayloadStart = true;\n            return;\n          }\n        }\n        int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead);\n        data.readBytes(sectionData.data, bytesRead, headerBytesToRead);\n        bytesRead += headerBytesToRead;\n        if (bytesRead == SECTION_HEADER_LENGTH) {\n          sectionData.reset(SECTION_HEADER_LENGTH);\n          sectionData.skipBytes(1); // Skip table id (8).\n          int secondHeaderByte = sectionData.readUnsignedByte();\n          int thirdHeaderByte = sectionData.readUnsignedByte();\n          sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0;\n          totalSectionLength =\n              (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH;\n          if (sectionData.capacity() < totalSectionLength) {\n            // Ensure there is enough space to keep the whole section.\n            byte[] bytes = sectionData.data;\n            sectionData.reset(\n                Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2)));\n            System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH);\n          }\n        }\n      } else {\n        // Reading the body.\n        int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead);\n        data.readBytes(sectionData.data, bytesRead, bodyBytesToRead);\n        bytesRead += bodyBytesToRead;\n        if (bytesRead == totalSectionLength) {\n          if (sectionSyntaxIndicator) {\n            // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11.\n            if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) {\n              // The CRC is invalid so discard the section.\n              waitingForPayloadStart = true;\n              return;\n            }\n            sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field.\n          } else {\n            // This is a private section with private defined syntax.\n            sectionData.reset(totalSectionLength);\n          }\n          reader.consume(sectionData);\n          bytesRead = 0;\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.text.cea.CeaUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.List;\n\n/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */\npublic final class SeiReader {\n\n  private final List<Format> closedCaptionFormats;\n  private final TrackOutput[] outputs;\n\n  /**\n   * @param closedCaptionFormats A list of formats for the closed caption channels to expose.\n   */\n  public SeiReader(List<Format> closedCaptionFormats) {\n    this.closedCaptionFormats = closedCaptionFormats;\n    outputs = new TrackOutput[closedCaptionFormats.size()];\n  }\n\n  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n    for (int i = 0; i < outputs.length; i++) {\n      idGenerator.generateNewId();\n      TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);\n      Format channelFormat = closedCaptionFormats.get(i);\n      String channelMimeType = channelFormat.sampleMimeType;\n      Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)\n          || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),\n          \"Invalid closed caption mime type provided: \" + channelMimeType);\n      String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId();\n      output.format(\n          Format.createTextSampleFormat(\n              formatId,\n              channelMimeType,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              channelFormat.selectionFlags,\n              channelFormat.language,\n              channelFormat.accessibilityChannel,\n              /* drmInitData= */ null,\n              Format.OFFSET_SAMPLE_RELATIVE,\n              channelFormat.initializationData));\n      outputs[i] = output;\n    }\n  }\n\n  public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {\n    CeaUtil.consume(pesTimeUs, seiBuffer, outputs);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\n\n/**\n * Parses splice info sections as defined by SCTE35.\n */\npublic final class SpliceInfoSectionReader implements SectionPayloadReader {\n\n  private TimestampAdjuster timestampAdjuster;\n  private TrackOutput output;\n  private boolean formatDeclared;\n\n  @Override\n  public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n      TsPayloadReader.TrackIdGenerator idGenerator) {\n    this.timestampAdjuster = timestampAdjuster;\n    idGenerator.generateNewId();\n    output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);\n    output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35,\n        null, Format.NO_VALUE, null));\n  }\n\n  @Override\n  public void consume(ParsableByteArray sectionData) {\n    if (!formatDeclared) {\n      if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {\n        // There is not enough information to initialize the timestamp adjuster.\n        return;\n      }\n      output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,\n          timestampAdjuster.getTimestampOffsetUs()));\n      formatDeclared = true;\n    }\n    int sampleSize = sectionData.bytesLeft();\n    output.sampleData(sectionData, sampleSize);\n    output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,\n        sampleSize, 0, null);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.BinarySearchSeeker;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A seeker that supports seeking within TS stream using binary search.\n *\n * <p>This seeker uses the first and last PCR values within the stream, as well as the stream\n * duration to interpolate the PCR value of the seeking position. Then it performs binary search\n * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the\n * target PCR.\n */\n/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {\n\n  private static final long SEEK_TOLERANCE_US = 100_000;\n  private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;\n  private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;\n\n  public TsBinarySearchSeeker(\n      TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {\n    super(\n        new DefaultSeekTimestampConverter(),\n        new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),\n        streamDurationUs,\n        /* floorTimePosition= */ 0,\n        /* ceilingTimePosition= */ streamDurationUs + 1,\n        /* floorBytePosition= */ 0,\n        /* ceilingBytePosition= */ inputLength,\n        /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,\n        MINIMUM_SEARCH_RANGE_BYTES);\n  }\n\n  /**\n   * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given\n   * position in a TS stream.\n   *\n   * <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link\n   * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to\n   * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target\n   * timestamp.\n   */\n  private static final class TsPcrSeeker implements TimestampSeeker {\n\n    private final TimestampAdjuster pcrTimestampAdjuster;\n    private final ParsableByteArray packetBuffer;\n    private final int pcrPid;\n\n    public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {\n      this.pcrPid = pcrPid;\n      this.pcrTimestampAdjuster = pcrTimestampAdjuster;\n      packetBuffer = new ParsableByteArray();\n    }\n\n    @Override\n    public TimestampSearchResult searchForTimestamp(\n        ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)\n        throws IOException, InterruptedException {\n      long inputPosition = input.getPosition();\n      int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);\n\n      packetBuffer.reset(bytesToSearch);\n      input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n      return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);\n    }\n\n    private TimestampSearchResult searchForPcrValueInBuffer(\n        ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {\n      int limit = packetBuffer.limit();\n\n      long startOfLastPacketPosition = C.POSITION_UNSET;\n      long endOfLastPacketPosition = C.POSITION_UNSET;\n      long lastPcrTimeUsInRange = C.TIME_UNSET;\n\n      while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {\n        int startOfPacket =\n            TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit);\n        int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;\n        if (endOfPacket > limit) {\n          break;\n        }\n        long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);\n        if (pcrValue != C.TIME_UNSET) {\n          long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);\n          if (pcrTimeUs > targetPcrTimeUs) {\n            if (lastPcrTimeUsInRange == C.TIME_UNSET) {\n              // First PCR timestamp is already over target.\n              return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);\n            } else {\n              // Last PCR timestamp < target timestamp < this timestamp.\n              return TimestampSearchResult.targetFoundResult(\n                  bufferStartOffset + startOfLastPacketPosition);\n            }\n          } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {\n            long startOfPacketInStream = bufferStartOffset + startOfPacket;\n            return TimestampSearchResult.targetFoundResult(startOfPacketInStream);\n          }\n\n          lastPcrTimeUsInRange = pcrTimeUs;\n          startOfLastPacketPosition = startOfPacket;\n        }\n        packetBuffer.setPosition(endOfPacket);\n        endOfLastPacketPosition = endOfPacket;\n      }\n\n      if (lastPcrTimeUsInRange != C.TIME_UNSET) {\n        long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;\n        return TimestampSearchResult.underestimatedResult(\n            lastPcrTimeUsInRange, endOfLastPacketPositionInStream);\n      } else {\n        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;\n      }\n    }\n\n    @Override\n    public void onSeekFinished() {\n      packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A reader that can extract the approximate duration from a given MPEG transport stream (TS).\n *\n * <p>This reader extracts the duration by reading PCR values of the PCR PID packets at the start\n * and at the end of the stream, calculating the difference, and converting that into stream\n * duration. This reader also handles the case when a single PCR wraparound takes place within the\n * stream, which can make PCR values at the beginning of the stream larger than PCR values at the\n * end. This class can only be used once to read duration from a given stream, and the usage of the\n * class is not thread-safe, so all calls should be made from the same thread.\n */\n/* package */ final class TsDurationReader {\n\n  private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;\n\n  private final TimestampAdjuster pcrTimestampAdjuster;\n  private final ParsableByteArray packetBuffer;\n\n  private boolean isDurationRead;\n  private boolean isFirstPcrValueRead;\n  private boolean isLastPcrValueRead;\n\n  private long firstPcrValue;\n  private long lastPcrValue;\n  private long durationUs;\n\n  /* package */ TsDurationReader() {\n    pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);\n    firstPcrValue = C.TIME_UNSET;\n    lastPcrValue = C.TIME_UNSET;\n    durationUs = C.TIME_UNSET;\n    packetBuffer = new ParsableByteArray();\n  }\n\n  /** Returns true if a TS duration has been read. */\n  public boolean isDurationReadFinished() {\n    return isDurationRead;\n  }\n\n  /**\n   * Reads a TS duration from the input, using the given PCR PID.\n   *\n   * <p>This reader reads the duration by reading PCR values of the PCR PID packets at the start and\n   * at the end of the stream, calculating the difference, and converting that into stream duration.\n   *\n   * @param input The {@link ExtractorInput} from which data should be read.\n   * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated\n   *     to hold the position of the required seek.\n   * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values.\n   * @return One of the {@code RESULT_} values defined in {@link Extractor}.\n   * @throws IOException If an error occurred reading from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public @Extractor.ReadResult int readDuration(\n      ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)\n      throws IOException, InterruptedException {\n    if (pcrPid <= 0) {\n      return finishReadDuration(input);\n    }\n    if (!isLastPcrValueRead) {\n      return readLastPcrValue(input, seekPositionHolder, pcrPid);\n    }\n    if (lastPcrValue == C.TIME_UNSET) {\n      return finishReadDuration(input);\n    }\n    if (!isFirstPcrValueRead) {\n      return readFirstPcrValue(input, seekPositionHolder, pcrPid);\n    }\n    if (firstPcrValue == C.TIME_UNSET) {\n      return finishReadDuration(input);\n    }\n\n    long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);\n    long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue);\n    durationUs = maxPcrPositionUs - minPcrPositionUs;\n    return finishReadDuration(input);\n  }\n\n  /**\n   * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}.\n   */\n  public long getDurationUs() {\n    return durationUs;\n  }\n\n  /**\n   * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the\n   * input TS stream.\n   */\n  public TimestampAdjuster getPcrTimestampAdjuster() {\n    return pcrTimestampAdjuster;\n  }\n\n  private int finishReadDuration(ExtractorInput input) {\n    packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);\n    isDurationRead = true;\n    input.resetPeekPosition();\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)\n      throws IOException, InterruptedException {\n    int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());\n    int searchStartPosition = 0;\n    if (input.getPosition() != searchStartPosition) {\n      seekPositionHolder.position = searchStartPosition;\n      return Extractor.RESULT_SEEK;\n    }\n\n    packetBuffer.reset(bytesToSearch);\n    input.resetPeekPosition();\n    input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n    firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);\n    isFirstPcrValueRead = true;\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {\n    int searchStartPosition = packetBuffer.getPosition();\n    int searchEndPosition = packetBuffer.limit();\n    for (int searchPosition = searchStartPosition;\n        searchPosition < searchEndPosition;\n        searchPosition++) {\n      if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {\n        continue;\n      }\n      long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);\n      if (pcrValue != C.TIME_UNSET) {\n        return pcrValue;\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n  private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)\n      throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);\n    long searchStartPosition = inputLength - bytesToSearch;\n    if (input.getPosition() != searchStartPosition) {\n      seekPositionHolder.position = searchStartPosition;\n      return Extractor.RESULT_SEEK;\n    }\n\n    packetBuffer.reset(bytesToSearch);\n    input.resetPeekPosition();\n    input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);\n\n    lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);\n    isLastPcrValueRead = true;\n    return Extractor.RESULT_CONTINUE;\n  }\n\n  private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {\n    int searchStartPosition = packetBuffer.getPosition();\n    int searchEndPosition = packetBuffer.limit();\n    for (int searchPosition = searchEndPosition - 1;\n        searchPosition >= searchStartPosition;\n        searchPosition--) {\n      if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {\n        continue;\n      }\n      long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);\n      if (pcrValue != C.TIME_UNSET) {\n        return pcrValue;\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;\n\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport android.util.SparseIntArray;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Extracts data from the MPEG-2 TS container format.\n */\npublic final class TsExtractor implements Extractor {\n\n  /** Factory for {@link TsExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()};\n\n  /**\n   * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link\n   * #MODE_HLS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS})\n  public @interface Mode {}\n\n  /**\n   * Behave as defined in ISO/IEC 13818-1.\n   */\n  public static final int MODE_MULTI_PMT = 0;\n  /**\n   * Assume only one PMT will be contained in the stream, even if more are declared by the PAT.\n   */\n  public static final int MODE_SINGLE_PMT = 1;\n  /**\n   * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore\n   * continuity counters.\n   */\n  public static final int MODE_HLS = 2;\n\n  public static final int TS_STREAM_TYPE_MPA = 0x03;\n  public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;\n  public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;\n  public static final int TS_STREAM_TYPE_AAC_LATM = 0x11;\n  public static final int TS_STREAM_TYPE_AC3 = 0x81;\n  public static final int TS_STREAM_TYPE_DTS = 0x8A;\n  public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;\n  public static final int TS_STREAM_TYPE_E_AC3 = 0x87;\n  public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor\n  public static final int TS_STREAM_TYPE_H262 = 0x02;\n  public static final int TS_STREAM_TYPE_H264 = 0x1B;\n  public static final int TS_STREAM_TYPE_H265 = 0x24;\n  public static final int TS_STREAM_TYPE_ID3 = 0x15;\n  public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;\n  public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;\n\n  public static final int TS_PACKET_SIZE = 188;\n  public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.\n\n  private static final int TS_PAT_PID = 0;\n  private static final int MAX_PID_PLUS_ONE = 0x2000;\n\n  private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33;\n  private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333;\n  private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34;\n  private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643;\n\n  private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50;\n  private static final int SNIFF_TS_PACKET_COUNT = 5;\n\n  private final @Mode int mode;\n  private final List<TimestampAdjuster> timestampAdjusters;\n  private final ParsableByteArray tsPacketBuffer;\n  private final SparseIntArray continuityCounters;\n  private final TsPayloadReader.Factory payloadReaderFactory;\n  private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid\n  private final SparseBooleanArray trackIds;\n  private final SparseBooleanArray trackPids;\n  private final TsDurationReader durationReader;\n\n  // Accessed only by the loading thread.\n  private TsBinarySearchSeeker tsBinarySearchSeeker;\n  private ExtractorOutput output;\n  private int remainingPmts;\n  private boolean tracksEnded;\n  private boolean hasOutputSeekMap;\n  private boolean pendingSeekToStart;\n  private TsPayloadReader id3Reader;\n  private int bytesSinceLastSync;\n  private int pcrPid;\n\n  public TsExtractor() {\n    this(0);\n  }\n\n  /**\n   * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}\n   *     {@code FLAG_*} values that control the behavior of the payload readers.\n   */\n  public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {\n    this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags);\n  }\n\n  /**\n   * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}\n   *     and {@link #MODE_HLS}.\n   * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}\n   *     {@code FLAG_*} values that control the behavior of the payload readers.\n   */\n  public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {\n    this(\n        mode,\n        new TimestampAdjuster(0),\n        new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));\n  }\n\n  /**\n   * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}\n   *     and {@link #MODE_HLS}.\n   * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.\n   * @param payloadReaderFactory Factory for injecting a custom set of payload readers.\n   */\n  public TsExtractor(\n      @Mode int mode,\n      TimestampAdjuster timestampAdjuster,\n      TsPayloadReader.Factory payloadReaderFactory) {\n    this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);\n    this.mode = mode;\n    if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {\n      timestampAdjusters = Collections.singletonList(timestampAdjuster);\n    } else {\n      timestampAdjusters = new ArrayList<>();\n      timestampAdjusters.add(timestampAdjuster);\n    }\n    tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);\n    trackIds = new SparseBooleanArray();\n    trackPids = new SparseBooleanArray();\n    tsPayloadReaders = new SparseArray<>();\n    continuityCounters = new SparseIntArray();\n    durationReader = new TsDurationReader();\n    pcrPid = -1;\n    resetPayloadReaders();\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    byte[] buffer = tsPacketBuffer.data;\n    input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);\n    for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) {\n      // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE.\n      boolean isSyncBytePatternCorrect = true;\n      for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) {\n        if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {\n          isSyncBytePatternCorrect = false;\n          break;\n        }\n      }\n      if (isSyncBytePatternCorrect) {\n        input.skipFully(startPosCandidate);\n        return true;\n      }\n    }\n    return false;\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.output = output;\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    Assertions.checkState(mode != MODE_HLS);\n    int timestampAdjustersCount = timestampAdjusters.size();\n    for (int i = 0; i < timestampAdjustersCount; i++) {\n      TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i);\n      boolean hasNotEncounteredFirstTimestamp =\n          timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;\n      if (hasNotEncounteredFirstTimestamp\n          || (timestampAdjuster.getTimestampOffsetUs() != 0\n              && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {\n        // - If a track in the TS stream has not encountered any sample, it's going to treat the\n        // first sample encountered as timestamp 0, which is incorrect. So we have to set the first\n        // sample timestamp for that track manually.\n        // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a\n        // different position, we need to set the first sample timestamp manually again.\n        timestampAdjuster.reset();\n        timestampAdjuster.setFirstSampleTimestampUs(timeUs);\n      }\n    }\n    if (timeUs != 0 && tsBinarySearchSeeker != null) {\n      tsBinarySearchSeeker.setSeekTargetUs(timeUs);\n    }\n    tsPacketBuffer.reset();\n    continuityCounters.clear();\n    for (int i = 0; i < tsPayloadReaders.size(); i++) {\n      tsPayloadReaders.valueAt(i).seek();\n    }\n    bytesSinceLastSync = 0;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    long inputLength = input.getLength();\n    if (tracksEnded) {\n      boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;\n      if (canReadDuration && !durationReader.isDurationReadFinished()) {\n        return durationReader.readDuration(input, seekPosition, pcrPid);\n      }\n      maybeOutputSeekMap(inputLength);\n\n      if (pendingSeekToStart) {\n        pendingSeekToStart = false;\n        seek(/* position= */ 0, /* timeUs= */ 0);\n        if (input.getPosition() != 0) {\n          seekPosition.position = 0;\n          return RESULT_SEEK;\n        }\n      }\n\n      if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {\n        return tsBinarySearchSeeker.handlePendingSeek(\n            input, seekPosition, /* outputFrameHolder= */ null);\n      }\n    }\n\n    if (!fillBufferWithAtLeastOnePacket(input)) {\n      return RESULT_END_OF_INPUT;\n    }\n\n    int endOfPacket = findEndOfFirstTsPacketInBuffer();\n    int limit = tsPacketBuffer.limit();\n    if (endOfPacket > limit) {\n      return RESULT_CONTINUE;\n    }\n\n    @TsPayloadReader.Flags int packetHeaderFlags = 0;\n\n    // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.\n    int tsPacketHeader = tsPacketBuffer.readInt();\n    if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator\n      // There are uncorrectable errors in this packet.\n      tsPacketBuffer.setPosition(endOfPacket);\n      return RESULT_CONTINUE;\n    }\n    packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;\n    // Ignoring transport_priority (tsPacketHeader & 0x200000)\n    int pid = (tsPacketHeader & 0x1FFF00) >> 8;\n    // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)\n    boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;\n    boolean payloadExists = (tsPacketHeader & 0x10) != 0;\n\n    TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null;\n    if (payloadReader == null) {\n      tsPacketBuffer.setPosition(endOfPacket);\n      return RESULT_CONTINUE;\n    }\n\n    // Discontinuity check.\n    if (mode != MODE_HLS) {\n      int continuityCounter = tsPacketHeader & 0xF;\n      int previousCounter = continuityCounters.get(pid, continuityCounter - 1);\n      continuityCounters.put(pid, continuityCounter);\n      if (previousCounter == continuityCounter) {\n        // Duplicate packet found.\n        tsPacketBuffer.setPosition(endOfPacket);\n        return RESULT_CONTINUE;\n      } else if (continuityCounter != ((previousCounter + 1) & 0xF)) {\n        // Discontinuity found.\n        payloadReader.seek();\n      }\n    }\n\n    // Skip the adaptation field.\n    if (adaptationFieldExists) {\n      int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();\n      int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();\n\n      packetHeaderFlags |=\n          (adaptationFieldFlags & 0x40) != 0 // random_access_indicator.\n              ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR\n              : 0;\n      tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);\n    }\n\n    // Read the payload.\n    boolean wereTracksEnded = tracksEnded;\n    if (shouldConsumePacketPayload(pid)) {\n      tsPacketBuffer.setLimit(endOfPacket);\n      payloadReader.consume(tsPacketBuffer, packetHeaderFlags);\n      tsPacketBuffer.setLimit(limit);\n    }\n    if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {\n      // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning\n      // and read again to make sure we output all media, including any contained in packets prior\n      // to those containing the track information.\n      pendingSeekToStart = true;\n    }\n\n    tsPacketBuffer.setPosition(endOfPacket);\n    return RESULT_CONTINUE;\n  }\n\n  // Internals.\n\n  private void maybeOutputSeekMap(long inputLength) {\n    if (!hasOutputSeekMap) {\n      hasOutputSeekMap = true;\n      if (durationReader.getDurationUs() != C.TIME_UNSET) {\n        tsBinarySearchSeeker =\n            new TsBinarySearchSeeker(\n                durationReader.getPcrTimestampAdjuster(),\n                durationReader.getDurationUs(),\n                inputLength,\n                pcrPid);\n        output.seekMap(tsBinarySearchSeeker.getSeekMap());\n      } else {\n        output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));\n      }\n    }\n  }\n\n  private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input)\n      throws IOException, InterruptedException {\n    byte[] data = tsPacketBuffer.data;\n    // Shift bytes to the start of the buffer if there isn't enough space left at the end.\n    if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {\n      int bytesLeft = tsPacketBuffer.bytesLeft();\n      if (bytesLeft > 0) {\n        System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);\n      }\n      tsPacketBuffer.reset(data, bytesLeft);\n    }\n    // Read more bytes until we have at least one packet.\n    while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {\n      int limit = tsPacketBuffer.limit();\n      int read = input.read(data, limit, BUFFER_SIZE - limit);\n      if (read == C.RESULT_END_OF_INPUT) {\n        return false;\n      }\n      tsPacketBuffer.setLimit(limit + read);\n    }\n    return true;\n  }\n\n  /**\n   * Returns the position of the end of the first TS packet (exclusive) in the packet buffer.\n   *\n   * <p>This may be a position beyond the buffer limit if the packet has not been read fully into\n   * the buffer, or if no packet could be found within the buffer.\n   */\n  private int findEndOfFirstTsPacketInBuffer() throws ParserException {\n    int searchStart = tsPacketBuffer.getPosition();\n    int limit = tsPacketBuffer.limit();\n    int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit);\n    // Discard all bytes before the sync byte.\n    // If sync byte is not found, this means discard the whole buffer.\n    tsPacketBuffer.setPosition(syncBytePosition);\n    int endOfPacket = syncBytePosition + TS_PACKET_SIZE;\n    if (endOfPacket > limit) {\n      bytesSinceLastSync += syncBytePosition - searchStart;\n      if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) {\n        throw new ParserException(\"Cannot find sync byte. Most likely not a Transport Stream.\");\n      }\n    } else {\n      // We have found a packet within the buffer.\n      bytesSinceLastSync = 0;\n    }\n    return endOfPacket;\n  }\n\n  private boolean shouldConsumePacketPayload(int packetPid) {\n    return mode == MODE_HLS\n        || tracksEnded\n        || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet\n  }\n\n  private void resetPayloadReaders() {\n    trackIds.clear();\n    tsPayloadReaders.clear();\n    SparseArray<TsPayloadReader> initialPayloadReaders =\n        payloadReaderFactory.createInitialPayloadReaders();\n    int initialPayloadReadersSize = initialPayloadReaders.size();\n    for (int i = 0; i < initialPayloadReadersSize; i++) {\n      tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));\n    }\n    tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));\n    id3Reader = null;\n  }\n\n  /**\n   * Parses Program Association Table data.\n   */\n  private class PatReader implements SectionPayloadReader {\n\n    private final ParsableBitArray patScratch;\n\n    public PatReader() {\n      patScratch = new ParsableBitArray(new byte[4]);\n    }\n\n    @Override\n    public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n        TrackIdGenerator idGenerator) {\n      // Do nothing.\n    }\n\n    @Override\n    public void consume(ParsableByteArray sectionData) {\n      int tableId = sectionData.readUnsignedByte();\n      if (tableId != 0x00 /* program_association_section */) {\n        // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.\n        return;\n      }\n      // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12),\n      // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1),\n      // section_number (8), last_section_number (8)\n      sectionData.skipBytes(7);\n\n      int programCount = sectionData.bytesLeft() / 4;\n      for (int i = 0; i < programCount; i++) {\n        sectionData.readBytes(patScratch, 4);\n        int programNumber = patScratch.readBits(16);\n        patScratch.skipBits(3); // reserved (3)\n        if (programNumber == 0) {\n          patScratch.skipBits(13); // network_PID (13)\n        } else {\n          int pid = patScratch.readBits(13);\n          tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));\n          remainingPmts++;\n        }\n      }\n      if (mode != MODE_HLS) {\n        tsPayloadReaders.remove(TS_PAT_PID);\n      }\n    }\n\n  }\n\n  /**\n   * Parses Program Map Table.\n   */\n  private class PmtReader implements SectionPayloadReader {\n\n    private static final int TS_PMT_DESC_REGISTRATION = 0x05;\n    private static final int TS_PMT_DESC_ISO639_LANG = 0x0A;\n    private static final int TS_PMT_DESC_AC3 = 0x6A;\n    private static final int TS_PMT_DESC_EAC3 = 0x7A;\n    private static final int TS_PMT_DESC_DTS = 0x7B;\n    private static final int TS_PMT_DESC_DVB_EXT = 0x7F;\n    private static final int TS_PMT_DESC_DVBSUBS = 0x59;\n\n    private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15;\n\n    private final ParsableBitArray pmtScratch;\n    private final SparseArray<TsPayloadReader> trackIdToReaderScratch;\n    private final SparseIntArray trackIdToPidScratch;\n    private final int pid;\n\n    public PmtReader(int pid) {\n      pmtScratch = new ParsableBitArray(new byte[5]);\n      trackIdToReaderScratch = new SparseArray<>();\n      trackIdToPidScratch = new SparseIntArray();\n      this.pid = pid;\n    }\n\n    @Override\n    public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n        TrackIdGenerator idGenerator) {\n      // Do nothing.\n    }\n\n    @Override\n    public void consume(ParsableByteArray sectionData) {\n      int tableId = sectionData.readUnsignedByte();\n      if (tableId != 0x02 /* TS_program_map_section */) {\n        // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.\n        return;\n      }\n      // TimestampAdjuster assignment.\n      TimestampAdjuster timestampAdjuster;\n      if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) {\n        timestampAdjuster = timestampAdjusters.get(0);\n      } else {\n        timestampAdjuster = new TimestampAdjuster(\n            timestampAdjusters.get(0).getFirstSampleTimestampUs());\n        timestampAdjusters.add(timestampAdjuster);\n      }\n\n      // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)\n      sectionData.skipBytes(2);\n      int programNumber = sectionData.readUnsignedShort();\n\n      // Skip 3 bytes (24 bits), including:\n      // reserved (2), version_number (5), current_next_indicator (1), section_number (8),\n      // last_section_number (8)\n      sectionData.skipBytes(3);\n\n      sectionData.readBytes(pmtScratch, 2);\n      // reserved (3), PCR_PID (13)\n      pmtScratch.skipBits(3);\n      pcrPid = pmtScratch.readBits(13);\n\n      // Read program_info_length.\n      sectionData.readBytes(pmtScratch, 2);\n      pmtScratch.skipBits(4);\n      int programInfoLength = pmtScratch.readBits(12);\n\n      // Skip the descriptors.\n      sectionData.skipBytes(programInfoLength);\n\n      if (mode == MODE_HLS && id3Reader == null) {\n        // Setup an ID3 track regardless of whether there's a corresponding entry, in case one\n        // appears intermittently during playback. See [Internal: b/20261500].\n        EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, Util.EMPTY_BYTE_ARRAY);\n        id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);\n        id3Reader.init(timestampAdjuster, output,\n            new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));\n      }\n\n      trackIdToReaderScratch.clear();\n      trackIdToPidScratch.clear();\n      int remainingEntriesLength = sectionData.bytesLeft();\n      while (remainingEntriesLength > 0) {\n        sectionData.readBytes(pmtScratch, 5);\n        int streamType = pmtScratch.readBits(8);\n        pmtScratch.skipBits(3); // reserved\n        int elementaryPid = pmtScratch.readBits(13);\n        pmtScratch.skipBits(4); // reserved\n        int esInfoLength = pmtScratch.readBits(12); // ES_info_length.\n        EsInfo esInfo = readEsInfo(sectionData, esInfoLength);\n        if (streamType == 0x06) {\n          streamType = esInfo.streamType;\n        }\n        remainingEntriesLength -= esInfoLength + 5;\n\n        int trackId = mode == MODE_HLS ? streamType : elementaryPid;\n        if (trackIds.get(trackId)) {\n          continue;\n        }\n\n        TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader\n            : payloadReaderFactory.createPayloadReader(streamType, esInfo);\n        if (mode != MODE_HLS\n            || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {\n          trackIdToPidScratch.put(trackId, elementaryPid);\n          trackIdToReaderScratch.put(trackId, reader);\n        }\n      }\n\n      int trackIdCount = trackIdToPidScratch.size();\n      for (int i = 0; i < trackIdCount; i++) {\n        int trackId = trackIdToPidScratch.keyAt(i);\n        int trackPid = trackIdToPidScratch.valueAt(i);\n        trackIds.put(trackId, true);\n        trackPids.put(trackPid, true);\n        TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);\n        if (reader != null) {\n          if (reader != id3Reader) {\n            reader.init(timestampAdjuster, output,\n                new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));\n          }\n          tsPayloadReaders.put(trackPid, reader);\n        }\n      }\n\n      if (mode == MODE_HLS) {\n        if (!tracksEnded) {\n          output.endTracks();\n          remainingPmts = 0;\n          tracksEnded = true;\n        }\n      } else {\n        tsPayloadReaders.remove(pid);\n        remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1;\n        if (remainingPmts == 0) {\n          output.endTracks();\n          tracksEnded = true;\n        }\n      }\n    }\n\n    /**\n     * Returns the stream info read from the available descriptors. Sets {@code data}'s position to\n     * the end of the descriptors.\n     *\n     * @param data A buffer with its position set to the start of the first descriptor.\n     * @param length The length of descriptors to read from the current position in {@code data}.\n     * @return The stream info read from the available descriptors.\n     */\n    private EsInfo readEsInfo(ParsableByteArray data, int length) {\n      int descriptorsStartPosition = data.getPosition();\n      int descriptorsEndPosition = descriptorsStartPosition + length;\n      int streamType = -1;\n      String language = null;\n      List<DvbSubtitleInfo> dvbSubtitleInfos = null;\n      while (data.getPosition() < descriptorsEndPosition) {\n        int descriptorTag = data.readUnsignedByte();\n        int descriptorLength = data.readUnsignedByte();\n        int positionOfNextDescriptor = data.getPosition() + descriptorLength;\n        if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor\n          long formatIdentifier = data.readUnsignedInt();\n          if (formatIdentifier == AC3_FORMAT_IDENTIFIER) {\n            streamType = TS_STREAM_TYPE_AC3;\n          } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) {\n            streamType = TS_STREAM_TYPE_E_AC3;\n          } else if (formatIdentifier == AC4_FORMAT_IDENTIFIER) {\n            streamType = TS_STREAM_TYPE_AC4;\n          } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) {\n            streamType = TS_STREAM_TYPE_H265;\n          }\n        } else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468)\n          streamType = TS_STREAM_TYPE_AC3;\n        } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor\n          streamType = TS_STREAM_TYPE_E_AC3;\n        } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) {\n          // Extension descriptor in DVB (ETSI EN 300 468).\n          int descriptorTagExt = data.readUnsignedByte();\n          if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) {\n            // AC-4_descriptor in DVB (ETSI EN 300 468).\n            streamType = TS_STREAM_TYPE_AC4;\n          }\n        } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor\n          streamType = TS_STREAM_TYPE_DTS;\n        } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) {\n          language = data.readString(3).trim();\n          // Audio type is ignored.\n        } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) {\n          streamType = TS_STREAM_TYPE_DVBSUBS;\n          dvbSubtitleInfos = new ArrayList<>();\n          while (data.getPosition() < positionOfNextDescriptor) {\n            String dvbLanguage = data.readString(3).trim();\n            int dvbSubtitlingType = data.readUnsignedByte();\n            byte[] initializationData = new byte[4];\n            data.readBytes(initializationData, 0, 4);\n            dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType,\n                initializationData));\n          }\n        }\n        // Skip unused bytes of current descriptor.\n        data.skipBytes(positionOfNextDescriptor - data.getPosition());\n      }\n      data.setPosition(descriptorsEndPosition);\n      return new EsInfo(streamType, language, dvbSubtitleInfos,\n          Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport android.util.SparseArray;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Parses TS packet payload data.\n */\npublic interface TsPayloadReader {\n\n  /**\n   * Factory of {@link TsPayloadReader} instances.\n   */\n  interface Factory {\n\n    /**\n     * Returns the initial mapping from PIDs to payload readers.\n     * <p>\n     * This method allows the injection of payload readers for reserved PIDs, excluding PID 0.\n     *\n     * @return A {@link SparseArray} that maps PIDs to payload readers.\n     */\n    SparseArray<TsPayloadReader> createInitialPayloadReaders();\n\n    /**\n     * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information.\n     * May return null if the stream type is not supported.\n     *\n     * @param streamType Stream type value as defined in the PMT entry or associated descriptors.\n     * @param esInfo Information associated to the elementary stream provided in the PMT.\n     * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid.\n     *     {@code null} if the stream is not supported.\n     */\n    TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);\n\n  }\n\n  /**\n   * Holds information associated with a PMT entry.\n   */\n  final class EsInfo {\n\n    public final int streamType;\n    public final String language;\n    public final List<DvbSubtitleInfo> dvbSubtitleInfos;\n    public final byte[] descriptorBytes;\n\n    /**\n     * @param streamType The type of the stream as defined by the\n     *     {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.\n     * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.\n     * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.\n     * @param descriptorBytes The descriptor bytes associated to the stream.\n     */\n    public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos,\n        byte[] descriptorBytes) {\n      this.streamType = streamType;\n      this.language = language;\n      this.dvbSubtitleInfos =\n          dvbSubtitleInfos == null\n              ? Collections.emptyList()\n              : Collections.unmodifiableList(dvbSubtitleInfos);\n      this.descriptorBytes = descriptorBytes;\n    }\n\n  }\n\n  /**\n   * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41.\n   */\n  final class DvbSubtitleInfo {\n\n    public final String language;\n    public final int type;\n    public final byte[] initializationData;\n\n    /**\n     * @param language The ISO 639-2 three-letter language code.\n     * @param type The subtitling type.\n     * @param initializationData The composition and ancillary page ids.\n     */\n    public DvbSubtitleInfo(String language, int type, byte[] initializationData) {\n      this.language = language;\n      this.type = type;\n      this.initializationData = initializationData;\n    }\n\n  }\n\n  /**\n   * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s.\n   */\n  final class TrackIdGenerator {\n\n    private static final int ID_UNSET = Integer.MIN_VALUE;\n\n    private final String formatIdPrefix;\n    private final int firstTrackId;\n    private final int trackIdIncrement;\n    private int trackId;\n    private String formatId;\n\n    public TrackIdGenerator(int firstTrackId, int trackIdIncrement) {\n      this(ID_UNSET, firstTrackId, trackIdIncrement);\n    }\n\n    public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) {\n      this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + \"/\" : \"\";\n      this.firstTrackId = firstTrackId;\n      this.trackIdIncrement = trackIdIncrement;\n      trackId = ID_UNSET;\n    }\n\n    /**\n     * Generates a new set of track and track format ids. Must be called before {@code get*}\n     * methods.\n     */\n    public void generateNewId() {\n      trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement;\n      formatId = formatIdPrefix + trackId;\n    }\n\n    /**\n     * Returns the last generated track id. Must be called after the first {@link #generateNewId()}\n     * call.\n     *\n     * @return The last generated track id.\n     */\n    public int getTrackId() {\n      maybeThrowUninitializedError();\n      return trackId;\n    }\n\n    /**\n     * Returns the last generated format id, with the format {@code \"programNumber/trackId\"}. If no\n     * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be\n     * called after the first {@link #generateNewId()} call.\n     *\n     * @return The last generated format id, with the format {@code \"programNumber/trackId\"}. If no\n     *     {@code programNumber} was provided, the {@code trackId} alone is used as\n     *     format id.\n     */\n    public String getFormatId() {\n      maybeThrowUninitializedError();\n      return formatId;\n    }\n\n    private void maybeThrowUninitializedError() {\n      if (trackId == ID_UNSET) {\n        throw new IllegalStateException(\"generateNewId() must be called before retrieving ids.\");\n      }\n    }\n\n  }\n\n  /**\n   * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        FLAG_PAYLOAD_UNIT_START_INDICATOR,\n        FLAG_RANDOM_ACCESS_INDICATOR,\n        FLAG_DATA_ALIGNMENT_INDICATOR\n      })\n  @interface Flags {}\n\n  /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */\n  int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1;\n  /**\n   * Indicates the presence of the random_access_indicator in the TS packet header adaptation field.\n   */\n  int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1;\n  /** Indicates the presence of the data_alignment_indicator in the PES header. */\n  int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2;\n\n  /**\n   * Initializes the payload reader.\n   *\n   * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.\n   * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.\n   * @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the\n   *     {@link TrackOutput}s.\n   */\n  void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n            TrackIdGenerator idGenerator);\n\n  /**\n   * Notifies the reader that a seek has occurred.\n   *\n   * <p>Following a call to this method, the data passed to the next invocation of {@link #consume}\n   * will not be a continuation of the data that was previously passed. Hence the reader should\n   * reset any internal state.\n   */\n  void seek();\n\n  /**\n   * Consumes the payload of a TS packet.\n   *\n   * @param data The TS packet. The position will be set to the start of the payload.\n   * @param flags See {@link Flags}.\n   * @throws ParserException If the payload could not be parsed.\n   */\n  void consume(ParsableByteArray data, @Flags int flags) throws ParserException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/** Utilities method for extracting MPEG-TS streams. */\npublic final class TsUtil {\n  /**\n   * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition)\n   * from the provided data array, or returns limitPosition if sync byte could not be found.\n   */\n  public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) {\n    int position = startPosition;\n    while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) {\n      position++;\n    }\n    return position;\n  }\n\n  /**\n   * Returns the PCR value read from a given TS packet.\n   *\n   * @param packetBuffer The buffer that holds the packet.\n   * @param startOfPacket The starting position of the packet in the buffer.\n   * @param pcrPid The PID for valid packets that contain PCR values.\n   * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it\n   *     contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise.\n   */\n  public static long readPcrFromPacket(\n      ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {\n    packetBuffer.setPosition(startOfPacket);\n    if (packetBuffer.bytesLeft() < 5) {\n      // Header = 4 bytes, adaptationFieldLength = 1 byte.\n      return C.TIME_UNSET;\n    }\n    // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.\n    int tsPacketHeader = packetBuffer.readInt();\n    if ((tsPacketHeader & 0x800000) != 0) {\n      // transport_error_indicator != 0 means there are uncorrectable errors in this packet.\n      return C.TIME_UNSET;\n    }\n    int pid = (tsPacketHeader & 0x1FFF00) >> 8;\n    if (pid != pcrPid) {\n      return C.TIME_UNSET;\n    }\n    boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;\n    if (!adaptationFieldExists) {\n      return C.TIME_UNSET;\n    }\n\n    int adaptationFieldLength = packetBuffer.readUnsignedByte();\n    if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {\n      int flags = packetBuffer.readUnsignedByte();\n      boolean pcrFlagSet = (flags & 0x10) == 0x10;\n      if (pcrFlagSet) {\n        byte[] pcrBytes = new byte[6];\n        packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);\n        return readPcrValueFromPcrBytes(pcrBytes);\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n  /**\n   * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes.\n   *\n   * <p>We ignore PCR Ext, because it's too small to have any significance.\n   */\n  private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {\n    return (pcrBytes[0] & 0xFFL) << 25\n        | (pcrBytes[1] & 0xFFL) << 17\n        | (pcrBytes[2] & 0xFFL) << 9\n        | (pcrBytes[3] & 0xFFL) << 1\n        | (pcrBytes[4] & 0xFFL) >> 7;\n  }\n\n  private TsUtil() {\n    // Prevent instantiation.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.ts;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.text.cea.CeaUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.List;\n\n/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */\n/* package */ final class UserDataReader {\n\n  private static final int USER_DATA_START_CODE = 0x0001B2;\n\n  private final List<Format> closedCaptionFormats;\n  private final TrackOutput[] outputs;\n\n  public UserDataReader(List<Format> closedCaptionFormats) {\n    this.closedCaptionFormats = closedCaptionFormats;\n    outputs = new TrackOutput[closedCaptionFormats.size()];\n  }\n\n  public void createTracks(\n      ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) {\n    for (int i = 0; i < outputs.length; i++) {\n      idGenerator.generateNewId();\n      TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);\n      Format channelFormat = closedCaptionFormats.get(i);\n      String channelMimeType = channelFormat.sampleMimeType;\n      Assertions.checkArgument(\n          MimeTypes.APPLICATION_CEA608.equals(channelMimeType)\n              || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),\n          \"Invalid closed caption mime type provided: \" + channelMimeType);\n      output.format(\n          Format.createTextSampleFormat(\n              idGenerator.getFormatId(),\n              channelMimeType,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              channelFormat.selectionFlags,\n              channelFormat.language,\n              channelFormat.accessibilityChannel,\n              /* drmInitData= */ null,\n              Format.OFFSET_SAMPLE_RELATIVE,\n              channelFormat.initializationData));\n      outputs[i] = output;\n    }\n  }\n\n  public void consume(long pesTimeUs, ParsableByteArray userDataPayload) {\n    if (userDataPayload.bytesLeft() < 9) {\n      return;\n    }\n    int userDataStartCode = userDataPayload.readInt();\n    int userDataIdentifier = userDataPayload.readInt();\n    int userDataTypeCode = userDataPayload.readUnsignedByte();\n    if (userDataStartCode == USER_DATA_START_CODE\n        && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94\n        && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) {\n      CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.wav;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.io.IOException;\n\n/**\n * Extracts data from WAV byte streams.\n */\npublic final class WavExtractor implements Extractor {\n\n  /** Factory for {@link WavExtractor} instances. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};\n\n  /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */\n  private static final int MAX_INPUT_SIZE = 32 * 1024;\n\n  private ExtractorOutput extractorOutput;\n  private TrackOutput trackOutput;\n  private WavHeader wavHeader;\n  private int bytesPerFrame;\n  private int pendingBytes;\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    return WavHeaderReader.peek(input) != null;\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    extractorOutput = output;\n    trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);\n    wavHeader = null;\n    output.endTracks();\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    pendingBytes = 0;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    if (wavHeader == null) {\n      wavHeader = WavHeaderReader.peek(input);\n      if (wavHeader == null) {\n        // Should only happen if the media wasn't sniffed.\n        throw new ParserException(\"Unsupported or unrecognized wav header.\");\n      }\n      Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null,\n          wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(),\n          wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null);\n      trackOutput.format(format);\n      bytesPerFrame = wavHeader.getBytesPerFrame();\n    }\n\n    if (!wavHeader.hasDataBounds()) {\n      WavHeaderReader.skipToData(input, wavHeader);\n      extractorOutput.seekMap(wavHeader);\n    } else if (input.getPosition() == 0) {\n      input.skipFully(wavHeader.getDataStartPosition());\n    }\n\n    long dataEndPosition = wavHeader.getDataEndPosition();\n    Assertions.checkState(dataEndPosition != C.POSITION_UNSET);\n\n    long bytesLeft = dataEndPosition - input.getPosition();\n    if (bytesLeft <= 0) {\n      return Extractor.RESULT_END_OF_INPUT;\n    }\n\n    int maxBytesToRead = (int) Math.min(MAX_INPUT_SIZE - pendingBytes, bytesLeft);\n    int bytesAppended = trackOutput.sampleData(input, maxBytesToRead, true);\n    if (bytesAppended != RESULT_END_OF_INPUT) {\n      pendingBytes += bytesAppended;\n    }\n\n    // Samples must consist of a whole number of frames.\n    int pendingFrames = pendingBytes / bytesPerFrame;\n    if (pendingFrames > 0) {\n      long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes);\n      int size = pendingFrames * bytesPerFrame;\n      pendingBytes -= size;\n      trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null);\n    }\n\n    return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.wav;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Header for a WAV file. */\n/* package */ final class WavHeader implements SeekMap {\n\n  /** Number of audio channels. */\n  private final int numChannels;\n  /** Sample rate in Hertz. */\n  private final int sampleRateHz;\n  /** Average bytes per second for the sample data. */\n  private final int averageBytesPerSecond;\n  /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */\n  private final int blockAlignment;\n  /** Bits per sample for the audio data. */\n  private final int bitsPerSample;\n  /** The PCM encoding. */\n  @C.PcmEncoding private final int encoding;\n\n  /** Position of the start of the sample data, in bytes. */\n  private int dataStartPosition;\n  /** Position of the end of the sample data (exclusive), in bytes. */\n  private long dataEndPosition;\n\n  public WavHeader(\n      int numChannels,\n      int sampleRateHz,\n      int averageBytesPerSecond,\n      int blockAlignment,\n      int bitsPerSample,\n      @C.PcmEncoding int encoding) {\n    this.numChannels = numChannels;\n    this.sampleRateHz = sampleRateHz;\n    this.averageBytesPerSecond = averageBytesPerSecond;\n    this.blockAlignment = blockAlignment;\n    this.bitsPerSample = bitsPerSample;\n    this.encoding = encoding;\n    dataStartPosition = C.POSITION_UNSET;\n    dataEndPosition = C.POSITION_UNSET;\n  }\n\n  // Data bounds.\n\n  /**\n   * Sets the data start position and size in bytes of sample data in this WAV.\n   *\n   * @param dataStartPosition The position of the start of the sample data, in bytes.\n   * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes.\n   */\n  public void setDataBounds(int dataStartPosition, long dataEndPosition) {\n    this.dataStartPosition = dataStartPosition;\n    this.dataEndPosition = dataEndPosition;\n  }\n\n  /**\n   * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if\n   * the data bounds have not been set.\n   */\n  public int getDataStartPosition() {\n    return dataStartPosition;\n  }\n\n  /**\n   * Returns the position of the end of the sample data (exclusive), in bytes, or {@link\n   * C#POSITION_UNSET} if the data bounds have not been set.\n   */\n  public long getDataEndPosition() {\n    return dataEndPosition;\n  }\n\n  /** Returns whether the data start position and size have been set. */\n  public boolean hasDataBounds() {\n    return dataStartPosition != C.POSITION_UNSET;\n  }\n\n  // SeekMap implementation.\n\n  @Override\n  public boolean isSeekable() {\n    return true;\n  }\n\n  @Override\n  public long getDurationUs() {\n    long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment;\n    return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;\n  }\n\n  @Override\n  public SeekPoints getSeekPoints(long timeUs) {\n    long dataSize = dataEndPosition - dataStartPosition;\n    long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;\n    // Constrain to nearest preceding frame offset.\n    positionOffset = (positionOffset / blockAlignment) * blockAlignment;\n    positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);\n    long seekPosition = dataStartPosition + positionOffset;\n    long seekTimeUs = getTimeUs(seekPosition);\n    SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);\n    if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) {\n      return new SeekPoints(seekPoint);\n    } else {\n      long secondSeekPosition = seekPosition + blockAlignment;\n      long secondSeekTimeUs = getTimeUs(secondSeekPosition);\n      SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);\n      return new SeekPoints(seekPoint, secondSeekPoint);\n    }\n  }\n\n  // Misc getters.\n\n  /**\n   * Returns the time in microseconds for the given position in bytes.\n   *\n   * @param position The position in bytes.\n   */\n  public long getTimeUs(long position) {\n    long positionOffset = Math.max(0, position - dataStartPosition);\n    return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond;\n  }\n\n  /** Returns the bytes per frame of this WAV. */\n  public int getBytesPerFrame() {\n    return blockAlignment;\n  }\n\n  /** Returns the bitrate of this WAV. */\n  public int getBitrate() {\n    return sampleRateHz * bitsPerSample * numChannels;\n  }\n\n  /** Returns the sample rate in Hertz of this WAV. */\n  public int getSampleRateHz() {\n    return sampleRateHz;\n  }\n\n  /** Returns the number of audio channels in this WAV. */\n  public int getNumChannels() {\n    return numChannels;\n  }\n\n  /** Returns the PCM encoding. **/\n  public @C.PcmEncoding int getEncoding() {\n    return encoding;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.extractor.wav;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.WavUtil;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */\n/* package */ final class WavHeaderReader {\n\n  private static final String TAG = \"WavHeaderReader\";\n\n  /**\n   * Peeks and returns a {@code WavHeader}.\n   *\n   * @param input Input stream to peek the WAV header from.\n   * @throws ParserException If the input file is an incorrect RIFF WAV.\n   * @throws IOException If peeking from the input fails.\n   * @throws InterruptedException If interrupted while peeking from input.\n   * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a\n   *     supported WAV format.\n   */\n  @Nullable\n  public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException {\n    Assertions.checkNotNull(input);\n\n    // Allocate a scratch buffer large enough to store the format chunk.\n    ParsableByteArray scratch = new ParsableByteArray(16);\n\n    // Attempt to read the RIFF chunk.\n    ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);\n    if (chunkHeader.id != WavUtil.RIFF_FOURCC) {\n      return null;\n    }\n\n    input.peekFully(scratch.data, 0, 4);\n    scratch.setPosition(0);\n    int riffFormat = scratch.readInt();\n    if (riffFormat != WavUtil.WAVE_FOURCC) {\n      Log.e(TAG, \"Unsupported RIFF format: \" + riffFormat);\n      return null;\n    }\n\n    // Skip chunks until we find the format chunk.\n    chunkHeader = ChunkHeader.peek(input, scratch);\n    while (chunkHeader.id != WavUtil.FMT_FOURCC) {\n      input.advancePeekPosition((int) chunkHeader.size);\n      chunkHeader = ChunkHeader.peek(input, scratch);\n    }\n\n    Assertions.checkState(chunkHeader.size >= 16);\n    input.peekFully(scratch.data, 0, 16);\n    scratch.setPosition(0);\n    int type = scratch.readLittleEndianUnsignedShort();\n    int numChannels = scratch.readLittleEndianUnsignedShort();\n    int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();\n    int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();\n    int blockAlignment = scratch.readLittleEndianUnsignedShort();\n    int bitsPerSample = scratch.readLittleEndianUnsignedShort();\n\n    int expectedBlockAlignment = numChannels * bitsPerSample / 8;\n    if (blockAlignment != expectedBlockAlignment) {\n      throw new ParserException(\"Expected block alignment: \" + expectedBlockAlignment + \"; got: \"\n          + blockAlignment);\n    }\n\n    @C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample);\n    if (encoding == C.ENCODING_INVALID) {\n      Log.e(TAG, \"Unsupported WAV format: \" + bitsPerSample + \" bit/sample, type \" + type);\n      return null;\n    }\n\n    // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...\n    input.advancePeekPosition((int) chunkHeader.size - 16);\n\n    return new WavHeader(\n        numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding);\n  }\n\n  /**\n   * Skips to the data in the given WAV input stream. After calling, the input stream's position\n   * will point to the start of sample data in the WAV, and the data bounds of the provided {@link\n   * WavHeader} will have been set.\n   *\n   * <p>If an exception is thrown, the input position will be left pointing to a chunk header and\n   * the bounds of the provided {@link WavHeader} will not have been set.\n   *\n   * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to a\n   *     valid chunk header.\n   * @param wavHeader WAV header to populate with data bounds.\n   * @throws ParserException If an error occurs parsing chunks.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from input.\n   */\n  public static void skipToData(ExtractorInput input, WavHeader wavHeader)\n      throws IOException, InterruptedException {\n    Assertions.checkNotNull(input);\n    Assertions.checkNotNull(wavHeader);\n\n    // Make sure the peek position is set to the read position before we peek the first header.\n    input.resetPeekPosition();\n\n    ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);\n    // Skip all chunks until we hit the data header.\n    ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);\n    while (chunkHeader.id != WavUtil.DATA_FOURCC) {\n      if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) {\n        Log.w(TAG, \"Ignoring unknown WAV chunk: \" + chunkHeader.id);\n      }\n      long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;\n      // Override size of RIFF chunk, since it describes its size as the entire file.\n      if (chunkHeader.id == WavUtil.RIFF_FOURCC) {\n        bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4;\n      }\n      if (bytesToSkip > Integer.MAX_VALUE) {\n        throw new ParserException(\"Chunk is too large (~2GB+) to skip; id: \" + chunkHeader.id);\n      }\n      input.skipFully((int) bytesToSkip);\n      chunkHeader = ChunkHeader.peek(input, scratch);\n    }\n    // Skip past the \"data\" header.\n    input.skipFully(ChunkHeader.SIZE_IN_BYTES);\n\n    int dataStartPosition = (int) input.getPosition();\n    long dataEndPosition = dataStartPosition + chunkHeader.size;\n    long inputLength = input.getLength();\n    if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {\n      Log.w(TAG, \"Data exceeds input length: \" + dataEndPosition + \", \" + inputLength);\n      dataEndPosition = inputLength;\n    }\n    wavHeader.setDataBounds(dataStartPosition, dataEndPosition);\n  }\n\n  private WavHeaderReader() {\n    // Prevent instantiation.\n  }\n\n  /** Container for a WAV chunk header. */\n  private static final class ChunkHeader {\n\n    /** Size in bytes of a WAV chunk header. */\n    public static final int SIZE_IN_BYTES = 8;\n\n    /** 4-character identifier, stored as an integer, for this chunk. */\n    public final int id;\n    /** Size of this chunk in bytes. */\n    public final long size;\n\n    private ChunkHeader(int id, long size) {\n      this.id = id;\n      this.size = size;\n    }\n\n    /**\n     * Peeks and returns a {@link ChunkHeader}.\n     *\n     * @param input Input stream to peek the chunk header from.\n     * @param scratch Buffer for temporary use.\n     * @throws IOException If peeking from the input fails.\n     * @throws InterruptedException If interrupted while peeking from input.\n     * @return A new {@code ChunkHeader} peeked from {@code input}.\n     */\n    public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch)\n        throws IOException, InterruptedException {\n      input.peekFully(scratch.data, 0, SIZE_IN_BYTES);\n      scratch.setPosition(0);\n\n      int id = scratch.readInt();\n      long size = scratch.readLittleEndianUnsignedInt();\n\n      return new ChunkHeader(id, size);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.mediacodec;\n\nimport android.annotation.TargetApi;\nimport android.graphics.Point;\nimport android.media.MediaCodec;\nimport android.media.MediaCodecInfo.AudioCapabilities;\nimport android.media.MediaCodecInfo.CodecCapabilities;\nimport android.media.MediaCodecInfo.CodecProfileLevel;\nimport android.media.MediaCodecInfo.VideoCapabilities;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Information about a {@link MediaCodec} for a given mime type. */\n@SuppressWarnings(\"InlinedApi\")\npublic final class MediaCodecInfo {\n\n  public static final String TAG = \"MediaCodecInfo\";\n\n  /**\n   * The value returned by {@link #getMaxSupportedInstances()} if the upper bound on the maximum\n   * number of supported instances is unknown.\n   */\n  public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1;\n\n  /**\n   * The name of the decoder.\n   * <p>\n   * May be passed to {@link MediaCodec#createByCodecName(String)} to create an instance of the\n   * decoder.\n   */\n  public final String name;\n\n  /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */\n  @Nullable public final String mimeType;\n\n  /**\n   * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this\n   * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a\n   * non-standard MIME type alias.\n   */\n  @Nullable public final String codecMimeType;\n\n  /**\n   * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not\n   * known.\n   */\n  @Nullable public final CodecCapabilities capabilities;\n\n  /**\n   * Whether the decoder supports seamless resolution switches.\n   *\n   * @see CodecCapabilities#isFeatureSupported(String)\n   * @see CodecCapabilities#FEATURE_AdaptivePlayback\n   */\n  public final boolean adaptive;\n\n  /**\n   * Whether the decoder supports tunneling.\n   *\n   * @see CodecCapabilities#isFeatureSupported(String)\n   * @see CodecCapabilities#FEATURE_TunneledPlayback\n   */\n  public final boolean tunneling;\n\n  /**\n   * Whether the decoder is secure.\n   *\n   * @see CodecCapabilities#isFeatureSupported(String)\n   * @see CodecCapabilities#FEATURE_SecurePlayback\n   */\n  public final boolean secure;\n\n  /** Whether this instance describes a passthrough codec. */\n  public final boolean passthrough;\n\n  /**\n   * Whether the codec is hardware accelerated.\n   *\n   * <p>This could be an approximation as the exact information is only provided in API levels 29+.\n   *\n   * @see android.media.MediaCodecInfo#isHardwareAccelerated()\n   */\n  public final boolean hardwareAccelerated;\n\n  /**\n   * Whether the codec is software only.\n   *\n   * <p>This could be an approximation as the exact information is only provided in API levels 29+.\n   *\n   * @see android.media.MediaCodecInfo#isSoftwareOnly()\n   */\n  public final boolean softwareOnly;\n\n  /**\n   * Whether the codec is from the vendor.\n   *\n   * <p>This could be an approximation as the exact information is only provided in API levels 29+.\n   *\n   * @see android.media.MediaCodecInfo#isVendor()\n   */\n  public final boolean vendor;\n\n  private final boolean isVideo;\n\n  /**\n   * Creates an instance representing an audio passthrough decoder.\n   *\n   * @param name The name of the {@link MediaCodec}.\n   * @return The created instance.\n   */\n  public static MediaCodecInfo newPassthroughInstance(String name) {\n    return new MediaCodecInfo(\n        name,\n        /* mimeType= */ null,\n        /* codecMimeType= */ null,\n        /* capabilities= */ null,\n        /* passthrough= */ true,\n        /* hardwareAccelerated= */ false,\n        /* softwareOnly= */ true,\n        /* vendor= */ false,\n        /* forceDisableAdaptive= */ false,\n        /* forceSecure= */ false);\n  }\n\n  /**\n   * Creates an instance.\n   *\n   * @param name The name of the {@link MediaCodec}.\n   * @param mimeType A mime type supported by the {@link MediaCodec}.\n   * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}.\n   *     Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias.\n   * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or\n   *     {@code null} if not known.\n   * @param hardwareAccelerated Whether the {@link MediaCodec} is hardware accelerated.\n   * @param softwareOnly Whether the {@link MediaCodec} is software only.\n   * @param vendor Whether the {@link MediaCodec} is provided by the vendor.\n   * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.\n   * @param forceSecure Whether {@link #secure} should be forced to {@code true}.\n   * @return The created instance.\n   */\n  public static MediaCodecInfo newInstance(\n      String name,\n      String mimeType,\n      String codecMimeType,\n      @Nullable CodecCapabilities capabilities,\n      boolean hardwareAccelerated,\n      boolean softwareOnly,\n      boolean vendor,\n      boolean forceDisableAdaptive,\n      boolean forceSecure) {\n    return new MediaCodecInfo(\n        name,\n        mimeType,\n        codecMimeType,\n        capabilities,\n        /* passthrough= */ false,\n        hardwareAccelerated,\n        softwareOnly,\n        vendor,\n        forceDisableAdaptive,\n        forceSecure);\n  }\n\n  private MediaCodecInfo(\n      String name,\n      @Nullable String mimeType,\n      @Nullable String codecMimeType,\n      @Nullable CodecCapabilities capabilities,\n      boolean passthrough,\n      boolean hardwareAccelerated,\n      boolean softwareOnly,\n      boolean vendor,\n      boolean forceDisableAdaptive,\n      boolean forceSecure) {\n    this.name = Assertions.checkNotNull(name);\n    this.mimeType = mimeType;\n    this.codecMimeType = codecMimeType;\n    this.capabilities = capabilities;\n    this.passthrough = passthrough;\n    this.hardwareAccelerated = hardwareAccelerated;\n    this.softwareOnly = softwareOnly;\n    this.vendor = vendor;\n    adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);\n    tunneling = capabilities != null && isTunneling(capabilities);\n    secure = forceSecure || (capabilities != null && isSecure(capabilities));\n    isVideo = MimeTypes.isVideo(mimeType);\n  }\n\n  @Override\n  public String toString() {\n    return name;\n  }\n\n  /**\n   * The profile levels supported by the decoder.\n   *\n   * @return The profile levels supported by the decoder.\n   */\n  public CodecProfileLevel[] getProfileLevels() {\n    return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0]\n        : capabilities.profileLevels;\n  }\n\n  /**\n   * Returns an upper bound on the maximum number of supported instances, or {@link\n   * #MAX_SUPPORTED_INSTANCES_UNKNOWN} if unknown. Applications should not expect to operate more\n   * instances than the returned maximum.\n   *\n   * @see CodecCapabilities#getMaxSupportedInstances()\n   */\n  public int getMaxSupportedInstances() {\n    return (Util.SDK_INT < 23 || capabilities == null)\n        ? MAX_SUPPORTED_INSTANCES_UNKNOWN\n        : getMaxSupportedInstancesV23(capabilities);\n  }\n\n  /**\n   * Returns whether the decoder may support decoding the given {@code format}.\n   *\n   * @param format The input media format.\n   * @return Whether the decoder may support decoding the given {@code format}.\n   * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders.\n   */\n  public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException {\n    if (!isCodecSupported(format)) {\n      return false;\n    }\n\n    if (isVideo) {\n      if (format.width <= 0 || format.height <= 0) {\n        return true;\n      }\n      if (Util.SDK_INT >= 21) {\n        return isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate);\n      } else {\n        boolean isFormatSupported =\n            format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize();\n        if (!isFormatSupported) {\n          logNoSupport(\"legacyFrameSize, \" + format.width + \"x\" + format.height);\n        }\n        return isFormatSupported;\n      }\n    } else { // Audio\n      return Util.SDK_INT < 21\n          || ((format.sampleRate == Format.NO_VALUE\n                  || isAudioSampleRateSupportedV21(format.sampleRate))\n              && (format.channelCount == Format.NO_VALUE\n                  || isAudioChannelCountSupportedV21(format.channelCount)));\n    }\n  }\n\n  /**\n   * Whether the decoder supports the codec of the given {@code format}. If there is insufficient\n   * information to decide, returns true.\n   *\n   * @param format The input media format.\n   * @return True if the codec of the given {@code format} is supported by the decoder.\n   */\n  public boolean isCodecSupported(Format format) {\n    if (format.codecs == null || mimeType == null) {\n      return true;\n    }\n    String codecMimeType = MimeTypes.getMediaMimeType(format.codecs);\n    if (codecMimeType == null) {\n      return true;\n    }\n    if (!mimeType.equals(codecMimeType)) {\n      logNoSupport(\"codec.mime \" + format.codecs + \", \" + codecMimeType);\n      return false;\n    }\n    Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);\n    if (codecProfileAndLevel == null) {\n      // If we don't know any better, we assume that the profile and level are supported.\n      return true;\n    }\n    int profile = codecProfileAndLevel.first;\n    int level = codecProfileAndLevel.second;\n    if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) {\n      // Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC\n      // which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145.\n      return true;\n    }\n    for (CodecProfileLevel capabilities : getProfileLevels()) {\n      if (capabilities.profile == profile && capabilities.level >= level) {\n        return true;\n      }\n    }\n    logNoSupport(\"codec.profileLevel, \" + format.codecs + \", \" + codecMimeType);\n    return false;\n  }\n\n  /** Whether the codec handles HDR10+ out-of-band metadata. */\n  public boolean isHdr10PlusOutOfBandMetadataSupported() {\n    if (Util.SDK_INT >= 29 && MimeTypes.VIDEO_VP9.equals(mimeType)) {\n      for (CodecProfileLevel capabilities : getProfileLevels()) {\n        if (capabilities.profile == CodecProfileLevel.VP9Profile2HDR10Plus) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Returns whether it may be possible to adapt to playing a different format when the codec is\n   * configured to play media in the specified {@code format}. For adaptation to succeed, the codec\n   * must also be configured with appropriate maximum values and {@link\n   * #isSeamlessAdaptationSupported(Format, Format, boolean)} must return {@code true} for the\n   * old/new formats.\n   *\n   * @param format The format of media for which the decoder will be configured.\n   * @return Whether adaptation may be possible\n   */\n  public boolean isSeamlessAdaptationSupported(Format format) {\n    if (isVideo) {\n      return adaptive;\n    } else {\n      Pair<Integer, Integer> codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format);\n      return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE;\n    }\n  }\n\n  /**\n   * Returns whether it is possible to adapt the decoder seamlessly from {@code oldFormat} to {@code\n   * newFormat}. If {@code newFormat} may not be completely populated, pass {@code false} for {@code\n   * isNewFormatComplete}.\n   *\n   * @param oldFormat The format being decoded.\n   * @param newFormat The new format.\n   * @param isNewFormatComplete Whether {@code newFormat} is populated with format-specific\n   *     metadata.\n   * @return Whether it is possible to adapt the decoder seamlessly.\n   */\n  public boolean isSeamlessAdaptationSupported(\n      Format oldFormat, Format newFormat, boolean isNewFormatComplete) {\n    if (isVideo) {\n      return oldFormat.sampleMimeType.equals(newFormat.sampleMimeType)\n          && oldFormat.rotationDegrees == newFormat.rotationDegrees\n          && (adaptive\n              || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height))\n          && ((!isNewFormatComplete && newFormat.colorInfo == null)\n              || Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo));\n    } else {\n      if (!MimeTypes.AUDIO_AAC.equals(mimeType)\n          || !oldFormat.sampleMimeType.equals(newFormat.sampleMimeType)\n          || oldFormat.channelCount != newFormat.channelCount\n          || oldFormat.sampleRate != newFormat.sampleRate) {\n        return false;\n      }\n      // Check the codec profile levels support adaptation.\n      Pair<Integer, Integer> oldCodecProfileLevel =\n          MediaCodecUtil.getCodecProfileAndLevel(oldFormat);\n      Pair<Integer, Integer> newCodecProfileLevel =\n          MediaCodecUtil.getCodecProfileAndLevel(newFormat);\n      if (oldCodecProfileLevel == null || newCodecProfileLevel == null) {\n        return false;\n      }\n      int oldProfile = oldCodecProfileLevel.first;\n      int newProfile = newCodecProfileLevel.first;\n      return oldProfile == CodecProfileLevel.AACObjectXHE\n          && newProfile == CodecProfileLevel.AACObjectXHE;\n    }\n  }\n\n  /**\n   * Whether the decoder supports video with a given width, height and frame rate.\n   *\n   * <p>Must not be called if the device SDK version is less than 21.\n   *\n   * @param width Width in pixels.\n   * @param height Height in pixels.\n   * @param frameRate Optional frame rate in frames per second. Ignored if set to {@link\n   *     Format#NO_VALUE} or any value less than or equal to 0.\n   * @return Whether the decoder supports video with the given width, height and frame rate.\n   */\n  @TargetApi(21)\n  public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) {\n    if (capabilities == null) {\n      logNoSupport(\"sizeAndRate.caps\");\n      return false;\n    }\n    VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();\n    if (videoCapabilities == null) {\n      logNoSupport(\"sizeAndRate.vCaps\");\n      return false;\n    }\n    if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) {\n      if (width >= height\n          || !enableRotatedVerticalResolutionWorkaround(name)\n          || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) {\n        logNoSupport(\"sizeAndRate.support, \" + width + \"x\" + height + \"x\" + frameRate);\n        return false;\n      }\n      logAssumedSupport(\"sizeAndRate.rotated, \" + width + \"x\" + height + \"x\" + frameRate);\n    }\n    return true;\n  }\n\n  /**\n   * Returns the smallest video size greater than or equal to a specified size that also satisfies\n   * the {@link MediaCodec}'s width and height alignment requirements.\n   * <p>\n   * Must not be called if the device SDK version is less than 21.\n   *\n   * @param width Width in pixels.\n   * @param height Height in pixels.\n   * @return The smallest video size greater than or equal to the specified size that also satisfies\n   *     the {@link MediaCodec}'s width and height alignment requirements, or null if not a video\n   *     codec.\n   */\n  @TargetApi(21)\n  public Point alignVideoSizeV21(int width, int height) {\n    if (capabilities == null) {\n      return null;\n    }\n    VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();\n    if (videoCapabilities == null) {\n      return null;\n    }\n    return alignVideoSizeV21(videoCapabilities, width, height);\n  }\n\n  /**\n   * Whether the decoder supports audio with a given sample rate.\n   * <p>\n   * Must not be called if the device SDK version is less than 21.\n   *\n   * @param sampleRate The sample rate in Hz.\n   * @return Whether the decoder supports audio with the given sample rate.\n   */\n  @TargetApi(21)\n  public boolean isAudioSampleRateSupportedV21(int sampleRate) {\n    if (capabilities == null) {\n      logNoSupport(\"sampleRate.caps\");\n      return false;\n    }\n    AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();\n    if (audioCapabilities == null) {\n      logNoSupport(\"sampleRate.aCaps\");\n      return false;\n    }\n    if (!audioCapabilities.isSampleRateSupported(sampleRate)) {\n      logNoSupport(\"sampleRate.support, \" + sampleRate);\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * Whether the decoder supports audio with a given channel count.\n   * <p>\n   * Must not be called if the device SDK version is less than 21.\n   *\n   * @param channelCount The channel count.\n   * @return Whether the decoder supports audio with the given channel count.\n   */\n  @TargetApi(21)\n  public boolean isAudioChannelCountSupportedV21(int channelCount) {\n    if (capabilities == null) {\n      logNoSupport(\"channelCount.caps\");\n      return false;\n    }\n    AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();\n    if (audioCapabilities == null) {\n      logNoSupport(\"channelCount.aCaps\");\n      return false;\n    }\n    int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType,\n        audioCapabilities.getMaxInputChannelCount());\n    if (maxInputChannelCount < channelCount) {\n      logNoSupport(\"channelCount.support, \" + channelCount);\n      return false;\n    }\n    return true;\n  }\n\n  private void logNoSupport(String message) {\n    Log.d(TAG, \"NoSupport [\" + message + \"] [\" + name + \", \" + mimeType + \"] [\"\n        + Util.DEVICE_DEBUG_INFO + \"]\");\n  }\n\n  private void logAssumedSupport(String message) {\n    Log.d(TAG, \"AssumedSupport [\" + message + \"] [\" + name + \", \" + mimeType + \"] [\"\n        + Util.DEVICE_DEBUG_INFO + \"]\");\n  }\n\n  private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) {\n    if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) {\n      // The maximum channel count looks like it's been set correctly.\n      return maxChannelCount;\n    }\n    if (MimeTypes.AUDIO_MPEG.equals(mimeType)\n        || MimeTypes.AUDIO_AMR_NB.equals(mimeType)\n        || MimeTypes.AUDIO_AMR_WB.equals(mimeType)\n        || MimeTypes.AUDIO_AAC.equals(mimeType)\n        || MimeTypes.AUDIO_VORBIS.equals(mimeType)\n        || MimeTypes.AUDIO_OPUS.equals(mimeType)\n        || MimeTypes.AUDIO_RAW.equals(mimeType)\n        || MimeTypes.AUDIO_FLAC.equals(mimeType)\n        || MimeTypes.AUDIO_ALAW.equals(mimeType)\n        || MimeTypes.AUDIO_MLAW.equals(mimeType)\n        || MimeTypes.AUDIO_MSGSM.equals(mimeType)) {\n      // Platform code should have set a default.\n      return maxChannelCount;\n    }\n    // The maximum channel count looks incorrect. Adjust it to an assumed default.\n    int assumedMaxChannelCount;\n    if (MimeTypes.AUDIO_AC3.equals(mimeType)) {\n      assumedMaxChannelCount = 6;\n    } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) {\n      assumedMaxChannelCount = 16;\n    } else {\n      // Default to the platform limit, which is 30.\n      assumedMaxChannelCount = 30;\n    }\n    Log.w(TAG, \"AssumedMaxChannelAdjustment: \" + name + \", [\" + maxChannelCount + \" to \"\n        + assumedMaxChannelCount + \"]\");\n    return assumedMaxChannelCount;\n  }\n\n  private static boolean isAdaptive(CodecCapabilities capabilities) {\n    return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities);\n  }\n\n  @TargetApi(19)\n  private static boolean isAdaptiveV19(CodecCapabilities capabilities) {\n    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);\n  }\n\n  private static boolean isTunneling(CodecCapabilities capabilities) {\n    return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);\n  }\n\n  @TargetApi(21)\n  private static boolean isTunnelingV21(CodecCapabilities capabilities) {\n    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);\n  }\n\n  private static boolean isSecure(CodecCapabilities capabilities) {\n    return Util.SDK_INT >= 21 && isSecureV21(capabilities);\n  }\n\n  @TargetApi(21)\n  private static boolean isSecureV21(CodecCapabilities capabilities) {\n    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);\n  }\n\n  @TargetApi(21)\n  private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,\n      int height, double frameRate) {\n    // Don't ever fail due to alignment. See: https://github.com/google/ExoPlayer/issues/6551.\n    Point alignedSize = alignVideoSizeV21(capabilities, width, height);\n    width = alignedSize.x;\n    height = alignedSize.y;\n\n    if (frameRate == Format.NO_VALUE || frameRate <= 0) {\n      return capabilities.isSizeSupported(width, height);\n    } else {\n      // The signaled frame rate may be slightly higher than the actual frame rate, so we take the\n      // floor to avoid situations where a range check in areSizeAndRateSupported fails due to\n      // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps).\n      double floorFrameRate = Math.floor(frameRate);\n      return capabilities.areSizeAndRateSupported(width, height, floorFrameRate);\n    }\n  }\n\n  @TargetApi(21)\n  private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width, int height) {\n    int widthAlignment = capabilities.getWidthAlignment();\n    int heightAlignment = capabilities.getHeightAlignment();\n    return new Point(\n        Util.ceilDivide(width, widthAlignment) * widthAlignment,\n        Util.ceilDivide(height, heightAlignment) * heightAlignment);\n  }\n\n  @TargetApi(23)\n  private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) {\n    return capabilities.getMaxSupportedInstances();\n  }\n\n  /**\n   * Capabilities are known to be inaccurately reported for vertical resolutions on some devices.\n   * [Internal ref: b/31387661]. When this workaround is enabled, we also check whether the\n   * capabilities indicate support if the width and height are swapped. If they do, we assume that\n   * the vertical resolution is also supported.\n   *\n   * @param name The name of the codec.\n   * @return Whether to enable the workaround.\n   */\n  private static final boolean enableRotatedVerticalResolutionWorkaround(String name) {\n    if (\"OMX.MTK.VIDEO.DECODER.HEVC\".equals(name) && \"mcv5a\".equals(Util.DEVICE)) {\n      // See https://github.com/google/ExoPlayer/issues/6612.\n      return false;\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.mediacodec;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaCodec;\nimport android.media.MediaCodec.CodecException;\nimport android.media.MediaCodec.CryptoException;\nimport android.media.MediaCrypto;\nimport android.media.MediaCryptoException;\nimport android.media.MediaFormat;\nimport android.os.Bundle;\nimport android.os.SystemClock;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.TimedValueQueue;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.\n */\npublic abstract class MediaCodecRenderer extends BaseRenderer {\n\n  /** Thrown when a failure occurs instantiating a decoder. */\n  public static class DecoderInitializationException extends Exception {\n\n    private static final int CUSTOM_ERROR_CODE_BASE = -50000;\n    private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;\n    private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;\n\n    /**\n     * The mime type for which a decoder was being initialized.\n     */\n    public final String mimeType;\n\n    /**\n     * Whether it was required that the decoder support a secure output path.\n     */\n    public final boolean secureDecoderRequired;\n\n    /**\n     * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable\n     * decoder was found.\n     */\n    @Nullable public final MediaCodecInfo codecInfo;\n\n    /** An optional developer-readable diagnostic information string. May be null. */\n    @Nullable public final String diagnosticInfo;\n\n    /**\n     * If the decoder failed to initialize and another decoder being used as a fallback also failed\n     * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if\n     * there was no fallback decoder or no suitable decoders were found.\n     */\n    @Nullable public final DecoderInitializationException fallbackDecoderInitializationException;\n\n    public DecoderInitializationException(Format format, Throwable cause,\n        boolean secureDecoderRequired, int errorCode) {\n      this(\n          \"Decoder init failed: [\" + errorCode + \"], \" + format,\n          cause,\n          format.sampleMimeType,\n          secureDecoderRequired,\n          /* mediaCodecInfo= */ null,\n          buildCustomDiagnosticInfo(errorCode),\n          /* fallbackDecoderInitializationException= */ null);\n    }\n\n    public DecoderInitializationException(\n        Format format,\n        Throwable cause,\n        boolean secureDecoderRequired,\n        MediaCodecInfo mediaCodecInfo) {\n      this(\n          \"Decoder init failed: \" + mediaCodecInfo.name + \", \" + format,\n          cause,\n          format.sampleMimeType,\n          secureDecoderRequired,\n          mediaCodecInfo,\n          Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null,\n          /* fallbackDecoderInitializationException= */ null);\n    }\n\n    private DecoderInitializationException(\n        String message,\n        Throwable cause,\n        String mimeType,\n        boolean secureDecoderRequired,\n        @Nullable MediaCodecInfo mediaCodecInfo,\n        @Nullable String diagnosticInfo,\n        @Nullable DecoderInitializationException fallbackDecoderInitializationException) {\n      super(message, cause);\n      this.mimeType = mimeType;\n      this.secureDecoderRequired = secureDecoderRequired;\n      this.codecInfo = mediaCodecInfo;\n      this.diagnosticInfo = diagnosticInfo;\n      this.fallbackDecoderInitializationException = fallbackDecoderInitializationException;\n    }\n\n    @CheckResult\n    private DecoderInitializationException copyWithFallbackException(\n        DecoderInitializationException fallbackException) {\n      return new DecoderInitializationException(\n          getMessage(),\n          getCause(),\n          mimeType,\n          secureDecoderRequired,\n          codecInfo,\n          diagnosticInfo,\n          fallbackException);\n    }\n\n    @TargetApi(21)\n    private static String getDiagnosticInfoV21(Throwable cause) {\n      if (cause instanceof CodecException) {\n        return ((CodecException) cause).getDiagnosticInfo();\n      }\n      return null;\n    }\n\n    private static String buildCustomDiagnosticInfo(int errorCode) {\n      String sign = errorCode < 0 ? \"neg_\" : \"\";\n      return \"com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_\"\n          + sign\n          + Math.abs(errorCode);\n    }\n  }\n\n  /** Thrown when a failure occurs in the decoder. */\n  public static class DecoderException extends Exception {\n\n    /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */\n    @Nullable public final MediaCodecInfo codecInfo;\n\n    /** An optional developer-readable diagnostic information string. May be null. */\n    @Nullable public final String diagnosticInfo;\n\n    public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) {\n      super(\"Decoder failed: \" + (codecInfo == null ? null : codecInfo.name), cause);\n      this.codecInfo = codecInfo;\n      diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;\n    }\n\n    @TargetApi(21)\n    private static String getDiagnosticInfoV21(Throwable cause) {\n      if (cause instanceof CodecException) {\n        return ((CodecException) cause).getDiagnosticInfo();\n      }\n      return null;\n    }\n  }\n\n  /** Indicates no codec operating rate should be set. */\n  protected static final float CODEC_OPERATING_RATE_UNSET = -1;\n\n  private static final String TAG = \"MediaCodecRenderer\";\n\n  /**\n   * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of\n   * time during which {@link #isReady()} will report true regardless of whether the new codec has\n   * output frames that are ready to be rendered.\n   * <p>\n   * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of\n   * other renderers, provided the new codec is able to decode some frames within this time period.\n   */\n  private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;\n\n  /**\n   * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format,\n   * Format)}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    KEEP_CODEC_RESULT_NO,\n    KEEP_CODEC_RESULT_YES_WITH_FLUSH,\n    KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION,\n    KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION\n  })\n  protected @interface KeepCodecResult {}\n  /** The codec cannot be kept. */\n  protected static final int KEEP_CODEC_RESULT_NO = 0;\n  /** The codec can be kept, but must be flushed. */\n  protected static final int KEEP_CODEC_RESULT_YES_WITH_FLUSH = 1;\n  /**\n   * The codec can be kept. It does not need to be flushed, but must be reconfigured by prefixing\n   * the next input buffer with the new format's configuration data.\n   */\n  protected static final int KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION = 2;\n  /** The codec can be kept. It does not need to be flushed and no reconfiguration is required. */\n  protected static final int KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION = 3;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    RECONFIGURATION_STATE_NONE,\n    RECONFIGURATION_STATE_WRITE_PENDING,\n    RECONFIGURATION_STATE_QUEUE_PENDING\n  })\n  private @interface ReconfigurationState {}\n  /**\n   * There is no pending adaptive reconfiguration work.\n   */\n  private static final int RECONFIGURATION_STATE_NONE = 0;\n  /**\n   * Codec configuration data needs to be written into the next buffer.\n   */\n  private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;\n  /**\n   * Codec configuration data has been written into the next buffer, but that buffer still needs to\n   * be returned to the codec.\n   */\n  private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({DRAIN_STATE_NONE, DRAIN_STATE_SIGNAL_END_OF_STREAM, DRAIN_STATE_WAIT_END_OF_STREAM})\n  private @interface DrainState {}\n  /** The codec is not being drained. */\n  private static final int DRAIN_STATE_NONE = 0;\n  /** The codec needs to be drained, but we haven't signaled an end of stream to it yet. */\n  private static final int DRAIN_STATE_SIGNAL_END_OF_STREAM = 1;\n  /** The codec needs to be drained, and we're waiting for it to output an end of stream. */\n  private static final int DRAIN_STATE_WAIT_END_OF_STREAM = 2;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    DRAIN_ACTION_NONE,\n    DRAIN_ACTION_FLUSH,\n    DRAIN_ACTION_UPDATE_DRM_SESSION,\n    DRAIN_ACTION_REINITIALIZE\n  })\n  private @interface DrainAction {}\n  /** No special action should be taken. */\n  private static final int DRAIN_ACTION_NONE = 0;\n  /** The codec should be flushed. */\n  private static final int DRAIN_ACTION_FLUSH = 1;\n  /** The codec should be flushed and updated to use the pending DRM session. */\n  private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;\n  /** The codec should be reinitialized. */\n  private static final int DRAIN_ACTION_REINITIALIZE = 3;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    ADAPTATION_WORKAROUND_MODE_NEVER,\n    ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION,\n    ADAPTATION_WORKAROUND_MODE_ALWAYS\n  })\n  private @interface AdaptationWorkaroundMode {}\n  /**\n   * The adaptation workaround is never used.\n   */\n  private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0;\n  /**\n   * The adaptation workaround is used when adapting between formats of the same resolution only.\n   */\n  private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1;\n  /**\n   * The adaptation workaround is always used when adapting between formats.\n   */\n  private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2;\n\n  /**\n   * H.264/AVC buffer to queue when using the adaptation workaround (see {@link\n   * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline\n   * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to\n   * force a resolution change when adapting to a new format.\n   */\n  private static final byte[] ADAPTATION_WORKAROUND_BUFFER =\n      new byte[] {\n        0, 0, 1, 103, 66, -64, 11, -38, 37, -112, 0, 0, 1, 104, -50, 15, 19, 32, 0, 0, 1, 101, -120,\n        -124, 13, -50, 113, 24, -96, 0, 47, -65, 28, 49, -61, 39, 93, 120\n      };\n\n  private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32;\n\n  private final MediaCodecSelector mediaCodecSelector;\n  @Nullable private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;\n  private final boolean playClearSamplesWithoutKeys;\n  private final boolean enableDecoderFallback;\n  private final float assumedMinimumCodecOperatingRate;\n  private final DecoderInputBuffer buffer;\n  private final DecoderInputBuffer flagsOnlyBuffer;\n  private final TimedValueQueue<Format> formatQueue;\n  private final ArrayList<Long> decodeOnlyPresentationTimestamps;\n  private final MediaCodec.BufferInfo outputBufferInfo;\n\n  @Nullable private Format inputFormat;\n  private Format outputFormat;\n  @Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;\n  @Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;\n  @Nullable private MediaCrypto mediaCrypto;\n  private boolean mediaCryptoRequiresSecureDecoder;\n  private long renderTimeLimitMs;\n  private float rendererOperatingRate;\n  @Nullable private MediaCodec codec;\n  @Nullable private Format codecFormat;\n  private float codecOperatingRate;\n  @Nullable private ArrayDeque<MediaCodecInfo> availableCodecInfos;\n  @Nullable private DecoderInitializationException preferredDecoderInitializationException;\n  @Nullable private MediaCodecInfo codecInfo;\n  @AdaptationWorkaroundMode private int codecAdaptationWorkaroundMode;\n  private boolean codecNeedsReconfigureWorkaround;\n  private boolean codecNeedsDiscardToSpsWorkaround;\n  private boolean codecNeedsFlushWorkaround;\n  private boolean codecNeedsEosFlushWorkaround;\n  private boolean codecNeedsEosOutputExceptionWorkaround;\n  private boolean codecNeedsMonoChannelCountWorkaround;\n  private boolean codecNeedsAdaptationWorkaroundBuffer;\n  private boolean shouldSkipAdaptationWorkaroundOutputBuffer;\n  private boolean codecNeedsEosPropagation;\n  private ByteBuffer[] inputBuffers;\n  private ByteBuffer[] outputBuffers;\n  private long codecHotswapDeadlineMs;\n  private int inputIndex;\n  private int outputIndex;\n  private ByteBuffer outputBuffer;\n  private boolean isDecodeOnlyOutputBuffer;\n  private boolean isLastOutputBuffer;\n  private boolean codecReconfigured;\n  @ReconfigurationState private int codecReconfigurationState;\n  @DrainState private int codecDrainState;\n  @DrainAction private int codecDrainAction;\n  private boolean codecReceivedBuffers;\n  private boolean codecReceivedEos;\n  private long largestQueuedPresentationTimeUs;\n  private long lastBufferInStreamPresentationTimeUs;\n  private boolean inputStreamEnded;\n  private boolean outputStreamEnded;\n  private boolean waitingForKeys;\n  private boolean waitingForFirstSyncSample;\n  private boolean waitingForFirstSampleInFormat;\n  private boolean skipMediaCodecStopOnRelease;\n  private boolean pendingOutputEndOfStream;\n\n  protected DecoderCounters decoderCounters;\n\n  /**\n   * @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*}\n   *     constants defined in {@link C}.\n   * @param mediaCodecSelector A decoder selector.\n   * @param drmSessionManager For use with encrypted media. May be null if support for encrypted\n   *     media is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is less efficient or slower\n   *     than the primary decoder.\n   * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by\n   *     this renderer are assumed to meet implicitly (i.e. without the operating rate being set\n   *     explicitly using {@link MediaFormat#KEY_OPERATING_RATE}).\n   */\n  public MediaCodecRenderer(\n      int trackType,\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean enableDecoderFallback,\n      float assumedMinimumCodecOperatingRate) {\n    super(trackType);\n    this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);\n    this.drmSessionManager = drmSessionManager;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    this.enableDecoderFallback = enableDecoderFallback;\n    this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate;\n    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);\n    flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();\n    formatQueue = new TimedValueQueue<>();\n    decodeOnlyPresentationTimestamps = new ArrayList<>();\n    outputBufferInfo = new MediaCodec.BufferInfo();\n    codecReconfigurationState = RECONFIGURATION_STATE_NONE;\n    codecDrainState = DRAIN_STATE_NONE;\n    codecDrainAction = DRAIN_ACTION_NONE;\n    codecOperatingRate = CODEC_OPERATING_RATE_UNSET;\n    rendererOperatingRate = 1f;\n    renderTimeLimitMs = C.TIME_UNSET;\n  }\n\n  /**\n   * Set a limit on the time a single {@link #render(long, long)} call can spend draining and\n   * filling the decoder.\n   *\n   * <p>This method is experimental, and will be renamed or removed in a future release. It should\n   * only be called before the renderer is used.\n   *\n   * @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no\n   *     limit.\n   */\n  public void experimental_setRenderTimeLimitMs(long renderTimeLimitMs) {\n    this.renderTimeLimitMs = renderTimeLimitMs;\n  }\n\n  /**\n   * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released.\n   *\n   * <p>By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it\n   * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this\n   * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}.\n   *\n   * <p>This method is experimental, and will be renamed or removed in a future release. It should\n   * only be called before the renderer is used.\n   *\n   * @param enabled enable or disable the feature.\n   */\n  public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) {\n    skipMediaCodecStopOnRelease = enabled;\n  }\n\n  @Override\n  @AdaptiveSupport\n  public final int supportsMixedMimeTypeAdaptation() {\n    return ADAPTIVE_NOT_SEAMLESS;\n  }\n\n  @Override\n  @Capabilities\n  public final int supportsFormat(Format format) throws ExoPlaybackException {\n    try {\n      return supportsFormat(mediaCodecSelector, drmSessionManager, format);\n    } catch (DecoderQueryException e) {\n      throw createRendererException(e, format);\n    }\n  }\n\n  /**\n   * Returns the {@link Capabilities} for the given {@link Format}.\n   *\n   * @param mediaCodecSelector The decoder selector.\n   * @param drmSessionManager The renderer's {@link DrmSessionManager}.\n   * @param format The {@link Format}.\n   * @return The {@link Capabilities} for this {@link Format}.\n   * @throws DecoderQueryException If there was an error querying decoders.\n   */\n  @Capabilities\n  protected abstract int supportsFormat(\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      Format format)\n      throws DecoderQueryException;\n\n  /**\n   * Returns a list of decoders that can decode media in the specified format, in priority order.\n   *\n   * @param mediaCodecSelector The decoder selector.\n   * @param format The {@link Format} for which a decoder is required.\n   * @param requiresSecureDecoder Whether a secure decoder is required.\n   * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.\n   * @throws DecoderQueryException Thrown if there was an error querying decoders.\n   */\n  protected abstract List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)\n      throws DecoderQueryException;\n\n  /**\n   * Configures a newly created {@link MediaCodec}.\n   *\n   * @param codecInfo Information about the {@link MediaCodec} being configured.\n   * @param codec The {@link MediaCodec} to configure.\n   * @param format The {@link Format} for which the codec is being configured.\n   * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.\n   * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if\n   *     no codec operating rate should be set.\n   */\n  protected abstract void configureCodec(\n      MediaCodecInfo codecInfo,\n      MediaCodec codec,\n      Format format,\n      @Nullable MediaCrypto crypto,\n      float codecOperatingRate);\n\n  protected final void maybeInitCodec() throws ExoPlaybackException {\n    if (codec != null || inputFormat == null) {\n      // We have a codec already, or we don't have a format with which to instantiate one.\n      return;\n    }\n\n    setCodecDrmSession(sourceDrmSession);\n\n    String mimeType = inputFormat.sampleMimeType;\n    if (codecDrmSession != null) {\n      if (mediaCrypto == null) {\n        FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();\n        if (sessionMediaCrypto == null) {\n          DrmSessionException drmError = codecDrmSession.getError();\n          if (drmError != null) {\n            // Continue for now. We may be able to avoid failure if the session recovers, or if a\n            // new input format causes the session to be replaced before it's used.\n          } else {\n            // The drm session isn't open yet.\n            return;\n          }\n        } else {\n          try {\n            mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);\n          } catch (MediaCryptoException e) {\n            throw createRendererException(e, inputFormat);\n          }\n          mediaCryptoRequiresSecureDecoder =\n              !sessionMediaCrypto.forceAllowInsecureDecoderComponents\n                  && mediaCrypto.requiresSecureDecoderComponent(mimeType);\n        }\n      }\n      if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) {\n        @DrmSession.State int drmSessionState = codecDrmSession.getState();\n        if (drmSessionState == DrmSession.STATE_ERROR) {\n          throw createRendererException(codecDrmSession.getError(), inputFormat);\n        } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {\n          // Wait for keys.\n          return;\n        }\n      }\n    }\n\n    try {\n      maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);\n    } catch (DecoderInitializationException e) {\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {\n    return true;\n  }\n\n  /**\n   * Returns whether the codec needs the renderer to propagate the end-of-stream signal directly,\n   * rather than by using an end-of-stream buffer queued to the codec.\n   */\n  protected boolean getCodecNeedsEosPropagation() {\n    return false;\n  }\n\n  /**\n   * Polls the pending output format queue for a given buffer timestamp. If a format is present, it\n   * is removed and returned. Otherwise returns {@code null}. Subclasses should only call this\n   * method if they are taking over responsibility for output format propagation (e.g., when using\n   * video tunneling).\n   */\n  protected final @Nullable Format updateOutputFormatForTime(long presentationTimeUs) {\n    Format format = formatQueue.pollFloor(presentationTimeUs);\n    if (format != null) {\n      outputFormat = format;\n    }\n    return format;\n  }\n\n  protected final MediaCodec getCodec() {\n    return codec;\n  }\n\n  protected final @Nullable MediaCodecInfo getCodecInfo() {\n    return codecInfo;\n  }\n\n  @Override\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    decoderCounters = new DecoderCounters();\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    inputStreamEnded = false;\n    outputStreamEnded = false;\n    pendingOutputEndOfStream = false;\n    flushOrReinitializeCodec();\n    formatQueue.clear();\n  }\n\n  @Override\n  public final void setOperatingRate(float operatingRate) throws ExoPlaybackException {\n    rendererOperatingRate = operatingRate;\n    if (codec != null\n        && codecDrainAction != DRAIN_ACTION_REINITIALIZE\n        && getState() != STATE_DISABLED) {\n      updateCodecOperatingRate();\n    }\n  }\n\n  @Override\n  protected void onDisabled() {\n    inputFormat = null;\n    if (sourceDrmSession != null || codecDrmSession != null) {\n      // TODO: Do something better with this case.\n      onReset();\n    } else {\n      flushOrReleaseCodec();\n    }\n  }\n\n  @Override\n  protected void onReset() {\n    try {\n      releaseCodec();\n    } finally {\n      setSourceDrmSession(null);\n    }\n  }\n\n  protected void releaseCodec() {\n    availableCodecInfos = null;\n    codecInfo = null;\n    codecFormat = null;\n    resetInputBuffer();\n    resetOutputBuffer();\n    resetCodecBuffers();\n    waitingForKeys = false;\n    codecHotswapDeadlineMs = C.TIME_UNSET;\n    decodeOnlyPresentationTimestamps.clear();\n    largestQueuedPresentationTimeUs = C.TIME_UNSET;\n    lastBufferInStreamPresentationTimeUs = C.TIME_UNSET;\n    try {\n      if (codec != null) {\n        decoderCounters.decoderReleaseCount++;\n        try {\n          if (!skipMediaCodecStopOnRelease) {\n            codec.stop();\n          }\n        } finally {\n          codec.release();\n        }\n      }\n    } finally {\n      codec = null;\n      try {\n        if (mediaCrypto != null) {\n          mediaCrypto.release();\n        }\n      } finally {\n        mediaCrypto = null;\n        mediaCryptoRequiresSecureDecoder = false;\n        setCodecDrmSession(null);\n      }\n    }\n  }\n\n  @Override\n  protected void onStarted() {\n    // Do nothing. Overridden to remove throws clause.\n  }\n\n  @Override\n  protected void onStopped() {\n    // Do nothing. Overridden to remove throws clause.\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (pendingOutputEndOfStream) {\n      pendingOutputEndOfStream = false;\n      processEndOfStream();\n    }\n    try {\n      if (outputStreamEnded) {\n        renderToEndOfStream();\n        return;\n      }\n      if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) {\n        // We still don't have a format and can't make progress without one.\n        return;\n      }\n      // We have a format.\n      maybeInitCodec();\n      if (codec != null) {\n        long drainStartTimeMs = SystemClock.elapsedRealtime();\n        TraceUtil.beginSection(\"drainAndFeed\");\n        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}\n        while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}\n        TraceUtil.endSection();\n      } else {\n        decoderCounters.skippedInputBufferCount += skipSource(positionUs);\n        // We need to read any format changes despite not having a codec so that drmSession can be\n        // updated, and so that we have the most recent format should the codec be initialized. We\n        // may also reach the end of the stream. Note that readSource will not read a sample into a\n        // flags-only buffer.\n        readToFlagsOnlyBuffer(/* requireFormat= */ false);\n      }\n      decoderCounters.ensureUpdated();\n    } catch (IllegalStateException e) {\n      if (isMediaCodecException(e)) {\n        throw createRendererException(e, inputFormat);\n      }\n      throw e;\n    }\n  }\n\n  /**\n   * Flushes the codec. If flushing is not possible, the codec will be released and re-instantiated.\n   * This method is a no-op if the codec is {@code null}.\n   *\n   * <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link\n   * #maybeInitCodec()} if the codec needs to be re-instantiated.\n   *\n   * @return Whether the codec was released and reinitialized, rather than being flushed.\n   * @throws ExoPlaybackException If an error occurs re-instantiating the codec.\n   */\n  protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {\n    boolean released = flushOrReleaseCodec();\n    if (released) {\n      maybeInitCodec();\n    }\n    return released;\n  }\n\n  /**\n   * Flushes the codec. If flushing is not possible, the codec will be released. This method is a\n   * no-op if the codec is {@code null}.\n   *\n   * @return Whether the codec was released.\n   */\n  protected boolean flushOrReleaseCodec() {\n    if (codec == null) {\n      return false;\n    }\n    if (codecDrainAction == DRAIN_ACTION_REINITIALIZE\n        || codecNeedsFlushWorkaround\n        || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {\n      releaseCodec();\n      return true;\n    }\n\n    codec.flush();\n    resetInputBuffer();\n    resetOutputBuffer();\n    codecHotswapDeadlineMs = C.TIME_UNSET;\n    codecReceivedEos = false;\n    codecReceivedBuffers = false;\n    waitingForFirstSyncSample = true;\n    codecNeedsAdaptationWorkaroundBuffer = false;\n    shouldSkipAdaptationWorkaroundOutputBuffer = false;\n    isDecodeOnlyOutputBuffer = false;\n    isLastOutputBuffer = false;\n\n    waitingForKeys = false;\n    decodeOnlyPresentationTimestamps.clear();\n    largestQueuedPresentationTimeUs = C.TIME_UNSET;\n    lastBufferInStreamPresentationTimeUs = C.TIME_UNSET;\n    codecDrainState = DRAIN_STATE_NONE;\n    codecDrainAction = DRAIN_ACTION_NONE;\n    // Reconfiguration data sent shortly before the flush may not have been processed by the\n    // decoder. If the codec has been reconfigured we always send reconfiguration data again to\n    // guarantee that it's processed.\n    codecReconfigurationState =\n        codecReconfigured ? RECONFIGURATION_STATE_WRITE_PENDING : RECONFIGURATION_STATE_NONE;\n    return false;\n  }\n\n  protected DecoderException createDecoderException(\n      Throwable cause, @Nullable MediaCodecInfo codecInfo) {\n    return new DecoderException(cause, codecInfo);\n  }\n\n  /** Reads into {@link #flagsOnlyBuffer} and returns whether a {@link Format} was read. */\n  private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException {\n    FormatHolder formatHolder = getFormatHolder();\n    flagsOnlyBuffer.clear();\n    int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat);\n    if (result == C.RESULT_FORMAT_READ) {\n      onInputFormatChanged(formatHolder);\n      return true;\n    } else if (result == C.RESULT_BUFFER_READ && flagsOnlyBuffer.isEndOfStream()) {\n      inputStreamEnded = true;\n      processEndOfStream();\n    }\n    return false;\n  }\n\n  private void maybeInitCodecWithFallback(\n      MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)\n      throws DecoderInitializationException {\n    if (availableCodecInfos == null) {\n      try {\n        List<MediaCodecInfo> allAvailableCodecInfos =\n            getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);\n        availableCodecInfos = new ArrayDeque<>();\n        if (enableDecoderFallback) {\n          availableCodecInfos.addAll(allAvailableCodecInfos);\n        } else if (!allAvailableCodecInfos.isEmpty()) {\n          availableCodecInfos.add(allAvailableCodecInfos.get(0));\n        }\n        preferredDecoderInitializationException = null;\n      } catch (DecoderQueryException e) {\n        throw new DecoderInitializationException(\n            inputFormat,\n            e,\n            mediaCryptoRequiresSecureDecoder,\n            DecoderInitializationException.DECODER_QUERY_ERROR);\n      }\n    }\n\n    if (availableCodecInfos.isEmpty()) {\n      throw new DecoderInitializationException(\n          inputFormat,\n          /* cause= */ null,\n          mediaCryptoRequiresSecureDecoder,\n          DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);\n    }\n\n    while (codec == null) {\n      MediaCodecInfo codecInfo = availableCodecInfos.peekFirst();\n      if (!shouldInitCodec(codecInfo)) {\n        return;\n      }\n      try {\n        initCodec(codecInfo, crypto);\n      } catch (Exception e) {\n        Log.w(TAG, \"Failed to initialize decoder: \" + codecInfo, e);\n        // This codec failed to initialize, so fall back to the next codec in the list (if any). We\n        // won't try to use this codec again unless there's a format change or the renderer is\n        // disabled and re-enabled.\n        availableCodecInfos.removeFirst();\n        DecoderInitializationException exception =\n            new DecoderInitializationException(\n                inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo);\n        if (preferredDecoderInitializationException == null) {\n          preferredDecoderInitializationException = exception;\n        } else {\n          preferredDecoderInitializationException =\n              preferredDecoderInitializationException.copyWithFallbackException(exception);\n        }\n        if (availableCodecInfos.isEmpty()) {\n          throw preferredDecoderInitializationException;\n        }\n      }\n    }\n\n    availableCodecInfos = null;\n  }\n\n  private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)\n      throws DecoderQueryException {\n    List<MediaCodecInfo> codecInfos =\n        getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);\n    if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {\n      // The drm session indicates that a secure decoder is required, but the device does not\n      // have one. Assuming that supportsFormat indicated support for the media being played, we\n      // know that it does not require a secure output path. Most CDM implementations allow\n      // playback to proceed with a non-secure decoder in this case, so we try our luck.\n      codecInfos =\n          getDecoderInfos(mediaCodecSelector, inputFormat, /* requiresSecureDecoder= */ false);\n      if (!codecInfos.isEmpty()) {\n        Log.w(\n            TAG,\n            \"Drm session requires secure decoder for \"\n                + inputFormat.sampleMimeType\n                + \", but no secure decoder available. Trying to proceed with \"\n                + codecInfos\n                + \".\");\n      }\n    }\n    return codecInfos;\n  }\n\n  private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception {\n    long codecInitializingTimestamp;\n    long codecInitializedTimestamp;\n    MediaCodec codec = null;\n    String codecName = codecInfo.name;\n\n    float codecOperatingRate =\n        Util.SDK_INT < 23\n            ? CODEC_OPERATING_RATE_UNSET\n            : getCodecOperatingRateV23(rendererOperatingRate, inputFormat, getStreamFormats());\n    if (codecOperatingRate <= assumedMinimumCodecOperatingRate) {\n      codecOperatingRate = CODEC_OPERATING_RATE_UNSET;\n    }\n    try {\n      codecInitializingTimestamp = SystemClock.elapsedRealtime();\n      TraceUtil.beginSection(\"createCodec:\" + codecName);\n      codec = MediaCodec.createByCodecName(codecName);\n      TraceUtil.endSection();\n      TraceUtil.beginSection(\"configureCodec\");\n      configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate);\n      TraceUtil.endSection();\n      TraceUtil.beginSection(\"startCodec\");\n      codec.start();\n      TraceUtil.endSection();\n      codecInitializedTimestamp = SystemClock.elapsedRealtime();\n      getCodecBuffers(codec);\n    } catch (Exception e) {\n      if (codec != null) {\n        resetCodecBuffers();\n        codec.release();\n      }\n      throw e;\n    }\n\n    this.codec = codec;\n    this.codecInfo = codecInfo;\n    this.codecOperatingRate = codecOperatingRate;\n    codecFormat = inputFormat;\n    codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);\n    codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName);\n    codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat);\n    codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);\n    codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);\n    codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);\n    codecNeedsMonoChannelCountWorkaround =\n        codecNeedsMonoChannelCountWorkaround(codecName, codecFormat);\n    codecNeedsEosPropagation =\n        codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation();\n\n    resetInputBuffer();\n    resetOutputBuffer();\n    codecHotswapDeadlineMs =\n        getState() == STATE_STARTED\n            ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS)\n            : C.TIME_UNSET;\n    codecReconfigured = false;\n    codecReconfigurationState = RECONFIGURATION_STATE_NONE;\n    codecReceivedEos = false;\n    codecReceivedBuffers = false;\n    largestQueuedPresentationTimeUs = C.TIME_UNSET;\n    lastBufferInStreamPresentationTimeUs = C.TIME_UNSET;\n    codecDrainState = DRAIN_STATE_NONE;\n    codecDrainAction = DRAIN_ACTION_NONE;\n    codecNeedsAdaptationWorkaroundBuffer = false;\n    shouldSkipAdaptationWorkaroundOutputBuffer = false;\n    isDecodeOnlyOutputBuffer = false;\n    isLastOutputBuffer = false;\n    waitingForFirstSyncSample = true;\n\n    decoderCounters.decoderInitCount++;\n    long elapsed = codecInitializedTimestamp - codecInitializingTimestamp;\n    onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);\n  }\n\n  private boolean shouldContinueFeeding(long drainStartTimeMs) {\n    return renderTimeLimitMs == C.TIME_UNSET\n        || SystemClock.elapsedRealtime() - drainStartTimeMs < renderTimeLimitMs;\n  }\n\n  private void getCodecBuffers(MediaCodec codec) {\n    if (Util.SDK_INT < 21) {\n      inputBuffers = codec.getInputBuffers();\n      outputBuffers = codec.getOutputBuffers();\n    }\n  }\n\n  private void resetCodecBuffers() {\n    if (Util.SDK_INT < 21) {\n      inputBuffers = null;\n      outputBuffers = null;\n    }\n  }\n\n  private ByteBuffer getInputBuffer(int inputIndex) {\n    if (Util.SDK_INT >= 21) {\n      return codec.getInputBuffer(inputIndex);\n    } else {\n      return inputBuffers[inputIndex];\n    }\n  }\n\n  private ByteBuffer getOutputBuffer(int outputIndex) {\n    if (Util.SDK_INT >= 21) {\n      return codec.getOutputBuffer(outputIndex);\n    } else {\n      return outputBuffers[outputIndex];\n    }\n  }\n\n  private boolean hasOutputBuffer() {\n    return outputIndex >= 0;\n  }\n\n  private void resetInputBuffer() {\n    inputIndex = C.INDEX_UNSET;\n    buffer.data = null;\n  }\n\n  private void resetOutputBuffer() {\n    outputIndex = C.INDEX_UNSET;\n    outputBuffer = null;\n  }\n\n  private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {\n    DrmSession.replaceSession(sourceDrmSession, session);\n    sourceDrmSession = session;\n  }\n\n  private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {\n    DrmSession.replaceSession(codecDrmSession, session);\n    codecDrmSession = session;\n  }\n\n  /**\n   * @return Whether it may be possible to feed more input data.\n   * @throws ExoPlaybackException If an error occurs feeding the input buffer.\n   */\n  private boolean feedInputBuffer() throws ExoPlaybackException {\n    if (codec == null || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM || inputStreamEnded) {\n      return false;\n    }\n\n    if (inputIndex < 0) {\n      inputIndex = codec.dequeueInputBuffer(0);\n      if (inputIndex < 0) {\n        return false;\n      }\n      buffer.data = getInputBuffer(inputIndex);\n      buffer.clear();\n    }\n\n    if (codecDrainState == DRAIN_STATE_SIGNAL_END_OF_STREAM) {\n      // We need to re-initialize the codec. Send an end of stream signal to the existing codec so\n      // that it outputs any remaining buffers before we release it.\n      if (codecNeedsEosPropagation) {\n        // Do nothing.\n      } else {\n        codecReceivedEos = true;\n        codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);\n        resetInputBuffer();\n      }\n      codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM;\n      return false;\n    }\n\n    if (codecNeedsAdaptationWorkaroundBuffer) {\n      codecNeedsAdaptationWorkaroundBuffer = false;\n      buffer.data.put(ADAPTATION_WORKAROUND_BUFFER);\n      codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0);\n      resetInputBuffer();\n      codecReceivedBuffers = true;\n      return true;\n    }\n\n    int result;\n    FormatHolder formatHolder = getFormatHolder();\n    int adaptiveReconfigurationBytes = 0;\n    if (waitingForKeys) {\n      // We've already read an encrypted sample into buffer, and are waiting for keys.\n      result = C.RESULT_BUFFER_READ;\n    } else {\n      // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied\n      // at the start of the buffer that also contains the first frame in the new format.\n      if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {\n        for (int i = 0; i < codecFormat.initializationData.size(); i++) {\n          byte[] data = codecFormat.initializationData.get(i);\n          buffer.data.put(data);\n        }\n        codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;\n      }\n      adaptiveReconfigurationBytes = buffer.data.position();\n      result = readSource(formatHolder, buffer, false);\n    }\n\n    if (hasReadStreamToEnd()) {\n      // Notify output queue of the last buffer's timestamp.\n      lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs;\n    }\n\n    if (result == C.RESULT_NOTHING_READ) {\n      return false;\n    }\n    if (result == C.RESULT_FORMAT_READ) {\n      if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {\n        // We received two formats in a row. Clear the current buffer of any reconfiguration data\n        // associated with the first format.\n        buffer.clear();\n        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;\n      }\n      onInputFormatChanged(formatHolder);\n      return true;\n    }\n\n    // We've read a buffer.\n    if (buffer.isEndOfStream()) {\n      if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {\n        // We received a new format immediately before the end of the stream. We need to clear\n        // the corresponding reconfiguration data from the current buffer, but re-write it into\n        // a subsequent buffer if there are any (e.g. if the user seeks backwards).\n        buffer.clear();\n        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;\n      }\n      inputStreamEnded = true;\n      if (!codecReceivedBuffers) {\n        processEndOfStream();\n        return false;\n      }\n      try {\n        if (codecNeedsEosPropagation) {\n          // Do nothing.\n        } else {\n          codecReceivedEos = true;\n          codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);\n          resetInputBuffer();\n        }\n      } catch (CryptoException e) {\n        throw createRendererException(e, inputFormat);\n      }\n      return false;\n    }\n    if (waitingForFirstSyncSample && !buffer.isKeyFrame()) {\n      buffer.clear();\n      if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {\n        // The buffer we just cleared contained reconfiguration data. We need to re-write this\n        // data into a subsequent buffer (if there is one).\n        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;\n      }\n      return true;\n    }\n    waitingForFirstSyncSample = false;\n    boolean bufferEncrypted = buffer.isEncrypted();\n    waitingForKeys = shouldWaitForKeys(bufferEncrypted);\n    if (waitingForKeys) {\n      return false;\n    }\n    if (codecNeedsDiscardToSpsWorkaround && !bufferEncrypted) {\n      NalUnitUtil.discardToSps(buffer.data);\n      if (buffer.data.position() == 0) {\n        return true;\n      }\n      codecNeedsDiscardToSpsWorkaround = false;\n    }\n    try {\n      long presentationTimeUs = buffer.timeUs;\n      if (buffer.isDecodeOnly()) {\n        decodeOnlyPresentationTimestamps.add(presentationTimeUs);\n      }\n      if (waitingForFirstSampleInFormat) {\n        formatQueue.add(presentationTimeUs, inputFormat);\n        waitingForFirstSampleInFormat = false;\n      }\n      largestQueuedPresentationTimeUs =\n          Math.max(largestQueuedPresentationTimeUs, presentationTimeUs);\n\n      buffer.flip();\n      if (buffer.hasSupplementalData()) {\n        handleInputBufferSupplementalData(buffer);\n      }\n      onQueueInputBuffer(buffer);\n\n      if (bufferEncrypted) {\n        MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,\n            adaptiveReconfigurationBytes);\n        codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);\n      } else {\n        codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);\n      }\n      resetInputBuffer();\n      codecReceivedBuffers = true;\n      codecReconfigurationState = RECONFIGURATION_STATE_NONE;\n      decoderCounters.inputBufferCount++;\n    } catch (CryptoException e) {\n      throw createRendererException(e, inputFormat);\n    }\n    return true;\n  }\n\n  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {\n    if (codecDrmSession == null\n        || (!bufferEncrypted\n            && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = codecDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw createRendererException(codecDrmSession.getError(), inputFormat);\n    }\n    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;\n  }\n\n  /**\n   * Called when a {@link MediaCodec} has been created and configured.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param name The name of the codec that was initialized.\n   * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization\n   *     finished.\n   * @param initializationDurationMs The time taken to initialize the codec in milliseconds.\n   */\n  protected void onCodecInitialized(String name, long initializedTimestampMs,\n      long initializationDurationMs) {\n    // Do nothing.\n  }\n\n  /**\n   * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}.\n   *\n   * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}.\n   * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}.\n   */\n  @SuppressWarnings(\"unchecked\")\n  protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {\n    waitingForFirstSampleInFormat = true;\n    Format newFormat = Assertions.checkNotNull(formatHolder.format);\n    if (formatHolder.includesDrmSession) {\n      setSourceDrmSession((DrmSession<FrameworkMediaCrypto>) formatHolder.drmSession);\n    } else {\n      sourceDrmSession =\n          getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);\n    }\n    inputFormat = newFormat;\n\n    if (codec == null) {\n      maybeInitCodec();\n      return;\n    }\n\n    // We have an existing codec that we may need to reconfigure or re-initialize. If the existing\n    // codec instance is being kept then its operating rate may need to be updated.\n\n    if ((sourceDrmSession == null && codecDrmSession != null)\n        || (sourceDrmSession != null && codecDrmSession == null)\n        || (sourceDrmSession != null && !codecInfo.secure)\n        || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {\n      // We might need to switch between the clear and protected output paths, or we're using DRM\n      // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM\n      // session.\n      drainAndReinitializeCodec();\n      return;\n    }\n\n    switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {\n      case KEEP_CODEC_RESULT_NO:\n        drainAndReinitializeCodec();\n        break;\n      case KEEP_CODEC_RESULT_YES_WITH_FLUSH:\n        codecFormat = newFormat;\n        updateCodecOperatingRate();\n        if (sourceDrmSession != codecDrmSession) {\n          drainAndUpdateCodecDrmSession();\n        } else {\n          drainAndFlushCodec();\n        }\n        break;\n      case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:\n        if (codecNeedsReconfigureWorkaround) {\n          drainAndReinitializeCodec();\n        } else {\n          codecReconfigured = true;\n          codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;\n          codecNeedsAdaptationWorkaroundBuffer =\n              codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS\n                  || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION\n                      && newFormat.width == codecFormat.width\n                      && newFormat.height == codecFormat.height);\n          codecFormat = newFormat;\n          updateCodecOperatingRate();\n          if (sourceDrmSession != codecDrmSession) {\n            drainAndUpdateCodecDrmSession();\n          }\n        }\n        break;\n      case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:\n        codecFormat = newFormat;\n        updateCodecOperatingRate();\n        if (sourceDrmSession != codecDrmSession) {\n          drainAndUpdateCodecDrmSession();\n        }\n        break;\n      default:\n        throw new IllegalStateException(); // Never happens.\n    }\n  }\n\n  /**\n   * Called when the output {@link MediaFormat} of the {@link MediaCodec} changes.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param codec The {@link MediaCodec} instance.\n   * @param outputMediaFormat The new output {@link MediaFormat}.\n   * @throws ExoPlaybackException Thrown if an error occurs handling the new output media format.\n   */\n  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat)\n      throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Handles supplemental data associated with an input buffer.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param buffer The input buffer that is about to be queued.\n   * @throws ExoPlaybackException Thrown if an error occurs handling supplemental data.\n   */\n  protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer)\n      throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called immediately before an input buffer is queued into the codec.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param buffer The buffer to be queued.\n   */\n  protected void onQueueInputBuffer(DecoderInputBuffer buffer) {\n    // Do nothing.\n  }\n\n  /**\n   * Called when an output buffer is successfully processed.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param presentationTimeUs The timestamp associated with the output buffer.\n   */\n  protected void onProcessedOutputBuffer(long presentationTimeUs) {\n    // Do nothing.\n  }\n\n  /**\n   * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if\n   * it can whether it requires reconfiguration.\n   *\n   * <p>The default implementation returns {@link #KEEP_CODEC_RESULT_NO}.\n   *\n   * @param codec The existing {@link MediaCodec} instance.\n   * @param codecInfo A {@link MediaCodecInfo} describing the decoder.\n   * @param oldFormat The {@link Format} for which the existing instance is configured.\n   * @param newFormat The new {@link Format}.\n   * @return Whether the instance can be kept, and if it can whether it requires reconfiguration.\n   */\n  protected @KeepCodecResult int canKeepCodec(\n      MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {\n    return KEEP_CODEC_RESULT_NO;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return outputStreamEnded;\n  }\n\n  @Override\n  public boolean isReady() {\n    return inputFormat != null\n        && !waitingForKeys\n        && (isSourceReady()\n            || hasOutputBuffer()\n            || (codecHotswapDeadlineMs != C.TIME_UNSET\n                && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));\n  }\n\n  /**\n   * Returns the maximum time to block whilst waiting for a decoded output buffer.\n   *\n   * @return The maximum time to block, in microseconds.\n   */\n  protected long getDequeueOutputBufferTimeoutUs() {\n    return 0;\n  }\n\n  /**\n   * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate,\n   * current {@link Format} and set of possible stream formats.\n   *\n   * <p>The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}.\n   *\n   * @param operatingRate The renderer operating rate.\n   * @param format The {@link Format} for which the codec is being configured.\n   * @param streamFormats The possible stream formats.\n   * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating\n   *     rate should be set.\n   */\n  protected float getCodecOperatingRateV23(\n      float operatingRate, Format format, Format[] streamFormats) {\n    return CODEC_OPERATING_RATE_UNSET;\n  }\n\n  /**\n   * Updates the codec operating rate.\n   *\n   * @throws ExoPlaybackException If an error occurs releasing or initializing a codec.\n   */\n  private void updateCodecOperatingRate() throws ExoPlaybackException {\n    if (Util.SDK_INT < 23) {\n      return;\n    }\n\n    float newCodecOperatingRate =\n        getCodecOperatingRateV23(rendererOperatingRate, codecFormat, getStreamFormats());\n    if (codecOperatingRate == newCodecOperatingRate) {\n      // No change.\n    } else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) {\n      // The only way to clear the operating rate is to instantiate a new codec instance. See\n      // [Internal ref: b/71987865].\n      drainAndReinitializeCodec();\n    } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET\n        || newCodecOperatingRate > assumedMinimumCodecOperatingRate) {\n      // We need to set the operating rate, either because we've set it previously or because it's\n      // above the assumed minimum rate.\n      Bundle codecParameters = new Bundle();\n      codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate);\n      codec.setParameters(codecParameters);\n      codecOperatingRate = newCodecOperatingRate;\n    }\n  }\n\n  /** Starts draining the codec for flush. */\n  private void drainAndFlushCodec() {\n    if (codecReceivedBuffers) {\n      codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;\n      codecDrainAction = DRAIN_ACTION_FLUSH;\n    }\n  }\n\n  /**\n   * Starts draining the codec to update its DRM session. The update may occur immediately if no\n   * buffers have been queued to the codec.\n   *\n   * @throws ExoPlaybackException If an error occurs updating the codec's DRM session.\n   */\n  private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException {\n    if (Util.SDK_INT < 23) {\n      // The codec needs to be re-initialized to switch to the source DRM session.\n      drainAndReinitializeCodec();\n      return;\n    }\n    if (codecReceivedBuffers) {\n      codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;\n      codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION;\n    } else {\n      // Nothing has been queued to the decoder, so we can do the update immediately.\n      updateDrmSessionOrReinitializeCodecV23();\n    }\n  }\n\n  /**\n   * Starts draining the codec for re-initialization. Re-initialization may occur immediately if no\n   * buffers have been queued to the codec.\n   *\n   * @throws ExoPlaybackException If an error occurs re-initializing a codec.\n   */\n  private void drainAndReinitializeCodec() throws ExoPlaybackException {\n    if (codecReceivedBuffers) {\n      codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;\n      codecDrainAction = DRAIN_ACTION_REINITIALIZE;\n    } else {\n      // Nothing has been queued to the decoder, so we can re-initialize immediately.\n      reinitializeCodec();\n    }\n  }\n\n  /**\n   * @return Whether it may be possible to drain more output data.\n   * @throws ExoPlaybackException If an error occurs draining the output buffer.\n   */\n  private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)\n      throws ExoPlaybackException {\n    if (!hasOutputBuffer()) {\n      int outputIndex;\n      if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {\n        try {\n          outputIndex =\n              codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());\n        } catch (IllegalStateException e) {\n          processEndOfStream();\n          if (outputStreamEnded) {\n            // Release the codec, as it's in an error state.\n            releaseCodec();\n          }\n          return false;\n        }\n      } else {\n        outputIndex =\n            codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());\n      }\n\n      if (outputIndex < 0) {\n        if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {\n          processOutputFormat();\n          return true;\n        } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) {\n          processOutputBuffersChanged();\n          return true;\n        }\n        /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */\n        if (codecNeedsEosPropagation\n            && (inputStreamEnded || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM)) {\n          processEndOfStream();\n        }\n        return false;\n      }\n\n      // We've dequeued a buffer.\n      if (shouldSkipAdaptationWorkaroundOutputBuffer) {\n        shouldSkipAdaptationWorkaroundOutputBuffer = false;\n        codec.releaseOutputBuffer(outputIndex, false);\n        return true;\n      } else if (outputBufferInfo.size == 0\n          && (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {\n        // The dequeued buffer indicates the end of the stream. Process it immediately.\n        processEndOfStream();\n        return false;\n      }\n\n      this.outputIndex = outputIndex;\n      outputBuffer = getOutputBuffer(outputIndex);\n      // The dequeued buffer is a media buffer. Do some initial setup.\n      // It will be processed by calling processOutputBuffer (possibly multiple times).\n      if (outputBuffer != null) {\n        outputBuffer.position(outputBufferInfo.offset);\n        outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);\n      }\n      isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs);\n      isLastOutputBuffer =\n          lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs;\n      updateOutputFormatForTime(outputBufferInfo.presentationTimeUs);\n    }\n\n    boolean processedOutputBuffer;\n    if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {\n      try {\n        processedOutputBuffer =\n            processOutputBuffer(\n                positionUs,\n                elapsedRealtimeUs,\n                codec,\n                outputBuffer,\n                outputIndex,\n                outputBufferInfo.flags,\n                outputBufferInfo.presentationTimeUs,\n                isDecodeOnlyOutputBuffer,\n                isLastOutputBuffer,\n                outputFormat);\n      } catch (IllegalStateException e) {\n        processEndOfStream();\n        if (outputStreamEnded) {\n          // Release the codec, as it's in an error state.\n          releaseCodec();\n        }\n        return false;\n      }\n    } else {\n      processedOutputBuffer =\n          processOutputBuffer(\n              positionUs,\n              elapsedRealtimeUs,\n              codec,\n              outputBuffer,\n              outputIndex,\n              outputBufferInfo.flags,\n              outputBufferInfo.presentationTimeUs,\n              isDecodeOnlyOutputBuffer,\n              isLastOutputBuffer,\n              outputFormat);\n    }\n\n    if (processedOutputBuffer) {\n      onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);\n      boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;\n      resetOutputBuffer();\n      if (!isEndOfStream) {\n        return true;\n      }\n      processEndOfStream();\n    }\n\n    return false;\n  }\n\n  /** Processes a new output {@link MediaFormat}. */\n  private void processOutputFormat() throws ExoPlaybackException {\n    MediaFormat mediaFormat = codec.getOutputFormat();\n    if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER\n        && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT\n        && mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)\n            == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) {\n      // We assume this format changed event was caused by the adaptation workaround.\n      shouldSkipAdaptationWorkaroundOutputBuffer = true;\n      return;\n    }\n    if (codecNeedsMonoChannelCountWorkaround) {\n      mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);\n    }\n    onOutputFormatChanged(codec, mediaFormat);\n  }\n\n  /**\n   * Processes a change in the output buffers.\n   */\n  private void processOutputBuffersChanged() {\n    if (Util.SDK_INT < 21) {\n      outputBuffers = codec.getOutputBuffers();\n    }\n  }\n\n  /**\n   * Processes an output media buffer.\n   *\n   * <p>When a new {@link ByteBuffer} is passed to this method its position and limit delineate the\n   * data to be processed. The return value indicates whether the buffer was processed in full. If\n   * true is returned then the next call to this method will receive a new buffer to be processed.\n   * If false is returned then the same buffer will be passed to the next call. An implementation of\n   * this method is free to modify the buffer and can assume that the buffer will not be externally\n   * modified between successive calls. Hence an implementation can, for example, modify the\n   * buffer's position to keep track of how much of the data it has processed.\n   *\n   * <p>Note that the first call to this method following a call to {@link #onPositionReset(long,\n   * boolean)} will always receive a new {@link ByteBuffer} to be processed.\n   *\n   * @param positionUs The current media time in microseconds, measured at the start of the current\n   *     iteration of the rendering loop.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the\n   *     start of the current iteration of the rendering loop.\n   * @param codec The {@link MediaCodec} instance.\n   * @param buffer The output buffer to process.\n   * @param bufferIndex The index of the output buffer.\n   * @param bufferFlags The flags attached to the output buffer.\n   * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds.\n   * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY}\n   *     by the source.\n   * @param isLastBuffer Whether the buffer is the last sample of the current stream.\n   * @param format The {@link Format} associated with the buffer.\n   * @return Whether the output buffer was fully processed (e.g. rendered or skipped).\n   * @throws ExoPlaybackException If an error occurs processing the output buffer.\n   */\n  protected abstract boolean processOutputBuffer(\n      long positionUs,\n      long elapsedRealtimeUs,\n      MediaCodec codec,\n      ByteBuffer buffer,\n      int bufferIndex,\n      int bufferFlags,\n      long bufferPresentationTimeUs,\n      boolean isDecodeOnlyBuffer,\n      boolean isLastBuffer,\n      Format format)\n      throws ExoPlaybackException;\n\n  /**\n   * Incrementally renders any remaining output.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output.\n   */\n  protected void renderToEndOfStream() throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Processes an end of stream signal.\n   *\n   * @throws ExoPlaybackException If an error occurs processing the signal.\n   */\n  private void processEndOfStream() throws ExoPlaybackException {\n    switch (codecDrainAction) {\n      case DRAIN_ACTION_REINITIALIZE:\n        reinitializeCodec();\n        break;\n      case DRAIN_ACTION_UPDATE_DRM_SESSION:\n        updateDrmSessionOrReinitializeCodecV23();\n        break;\n      case DRAIN_ACTION_FLUSH:\n        flushOrReinitializeCodec();\n        break;\n      case DRAIN_ACTION_NONE:\n      default:\n        outputStreamEnded = true;\n        renderToEndOfStream();\n        break;\n    }\n  }\n\n  /**\n   * Notifies the renderer that output end of stream is pending and should be handled on the next\n   * render.\n   */\n  protected final void setPendingOutputEndOfStream() {\n    pendingOutputEndOfStream = true;\n  }\n\n  private void reinitializeCodec() throws ExoPlaybackException {\n    releaseCodec();\n    maybeInitCodec();\n  }\n\n  @TargetApi(23)\n  private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException {\n    FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto();\n    if (sessionMediaCrypto == null) {\n      // We'd only expect this to happen if the CDM from which the pending session is obtained needs\n      // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme\n      // to another, where the new CDM hasn't been used before and needs provisioning). It would be\n      // possible to handle this case more efficiently (i.e. with a new renderer state that waits\n      // for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra\n      // complexity is not warranted given how unlikely the case is to occur.\n      reinitializeCodec();\n      return;\n    }\n    if (C.PLAYREADY_UUID.equals(sessionMediaCrypto.uuid)) {\n      // The PlayReady CDM does not implement setMediaDrmSession.\n      // TODO: Add API check once [Internal ref: b/128835874] is fixed.\n      reinitializeCodec();\n      return;\n    }\n\n    if (flushOrReinitializeCodec()) {\n      // The codec was reinitialized. The new codec will be using the new DRM session, so there's\n      // nothing more to do.\n      return;\n    }\n\n    try {\n      mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId);\n    } catch (MediaCryptoException e) {\n      throw createRendererException(e, inputFormat);\n    }\n    setCodecDrmSession(sourceDrmSession);\n    codecDrainState = DRAIN_STATE_NONE;\n    codecDrainAction = DRAIN_ACTION_NONE;\n  }\n\n  private boolean isDecodeOnlyBuffer(long presentationTimeUs) {\n    // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would\n    // box presentationTimeUs, creating a Long object that would need to be garbage collected.\n    int size = decodeOnlyPresentationTimestamps.size();\n    for (int i = 0; i < size; i++) {\n      if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) {\n        decodeOnlyPresentationTimestamps.remove(i);\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(\n      DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {\n    MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo();\n    if (adaptiveReconfigurationBytes == 0) {\n      return cryptoInfo;\n    }\n    // There must be at least one sub-sample, although numBytesOfClearData is permitted to be\n    // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration\n    // bytes to the clear byte count of the first sub-sample.\n    if (cryptoInfo.numBytesOfClearData == null) {\n      cryptoInfo.numBytesOfClearData = new int[1];\n    }\n    cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;\n    return cryptoInfo;\n  }\n\n  private static boolean isMediaCodecException(IllegalStateException error) {\n    if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) {\n      return true;\n    }\n    StackTraceElement[] stackTrace = error.getStackTrace();\n    return stackTrace.length > 0 && stackTrace[0].getClassName().equals(\"android.media.MediaCodec\");\n  }\n\n  @TargetApi(21)\n  private static boolean isMediaCodecExceptionV21(IllegalStateException error) {\n    return error instanceof CodecException;\n  }\n\n  /**\n   * Returns whether the decoder is known to fail when flushed.\n   * <p>\n   * If true is returned, the renderer will work around the issue by releasing the decoder and\n   * instantiating a new one rather than flushing the current instance.\n   * <p>\n   * See [Internal: b/8347958, b/8543366].\n   *\n   * @param name The name of the decoder.\n   * @return True if the decoder is known to fail when flushed.\n   */\n  private static boolean codecNeedsFlushWorkaround(String name) {\n    return Util.SDK_INT < 18\n        || (Util.SDK_INT == 18\n        && (\"OMX.SEC.avc.dec\".equals(name) || \"OMX.SEC.avc.dec.secure\".equals(name)))\n        || (Util.SDK_INT == 19 && Util.MODEL.startsWith(\"SM-G800\")\n        && (\"OMX.Exynos.avc.dec\".equals(name) || \"OMX.Exynos.avc.dec.secure\".equals(name)));\n  }\n\n  /**\n   * Returns a mode that specifies when the adaptation workaround should be enabled.\n   *\n   * <p>When enabled, the workaround queues and discards a blank frame with a resolution whose width\n   * and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the decoder's\n   * internal state when a format change occurs.\n   *\n   * <p>See [Internal: b/27807182]. See <a\n   * href=\"https://github.com/google/ExoPlayer/issues/3257\">GitHub issue #3257</a>.\n   *\n   * @param name The name of the decoder.\n   * @return The mode specifying when the adaptation workaround should be enabled.\n   */\n  private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) {\n    if (Util.SDK_INT <= 25 && \"OMX.Exynos.avc.dec.secure\".equals(name)\n        && (Util.MODEL.startsWith(\"SM-T585\") || Util.MODEL.startsWith(\"SM-A510\")\n        || Util.MODEL.startsWith(\"SM-A520\") || Util.MODEL.startsWith(\"SM-J700\"))) {\n      return ADAPTATION_WORKAROUND_MODE_ALWAYS;\n    } else if (Util.SDK_INT < 24\n        && (\"OMX.Nvidia.h264.decode\".equals(name) || \"OMX.Nvidia.h264.decode.secure\".equals(name))\n        && (\"flounder\".equals(Util.DEVICE) || \"flounder_lte\".equals(Util.DEVICE)\n        || \"grouper\".equals(Util.DEVICE) || \"tilapia\".equals(Util.DEVICE))) {\n      return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION;\n    } else {\n      return ADAPTATION_WORKAROUND_MODE_NEVER;\n    }\n  }\n\n  /**\n   * Returns whether the decoder is known to fail when an attempt is made to reconfigure it with a\n   * new format's configuration data.\n   *\n   * <p>When enabled, the workaround will always release and recreate the decoder, rather than\n   * attempting to reconfigure the existing instance.\n   *\n   * @param name The name of the decoder.\n   * @return True if the decoder is known to fail when an attempt is made to reconfigure it with a\n   *     new format's configuration data.\n   */\n  private static boolean codecNeedsReconfigureWorkaround(String name) {\n    return Util.MODEL.startsWith(\"SM-T230\") && \"OMX.MARVELL.VIDEO.HW.CODA7542DECODER\".equals(name);\n  }\n\n  /**\n   * Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued\n   * before the codec specific data.\n   *\n   * <p>If true is returned, the renderer will work around the issue by discarding data up to the\n   * SPS.\n   *\n   * @param name The name of the decoder.\n   * @param format The {@link Format} used to configure the decoder.\n   * @return True if the decoder is known to fail if NAL units are queued before CSD.\n   */\n  private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format format) {\n    return Util.SDK_INT < 21 && format.initializationData.isEmpty()\n        && \"OMX.MTK.VIDEO.DECODER.AVC\".equals(name);\n  }\n\n  /**\n   * Returns whether the decoder is known to handle the propagation of the {@link\n   * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.\n   *\n   * <p>If true is returned, the renderer will work around the issue by approximating end of stream\n   * behavior without relying on the flag being propagated through to an output buffer by the\n   * underlying decoder.\n   *\n   * @param codecInfo Information about the {@link MediaCodec}.\n   * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}\n   *     propagation incorrectly on the host device. False otherwise.\n   */\n  private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {\n    String name = codecInfo.name;\n    return (Util.SDK_INT <= 25 && \"OMX.rk.video_decoder.avc\".equals(name))\n        || (Util.SDK_INT <= 17 && \"OMX.allwinner.video.decoder.avc\".equals(name))\n        || (\"Amazon\".equals(Util.MANUFACTURER) && \"AFTS\".equals(Util.MODEL) && codecInfo.secure);\n  }\n\n  /**\n   * Returns whether the decoder is known to behave incorrectly if flushed after receiving an input\n   * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.\n   * <p>\n   * If true is returned, the renderer will work around the issue by instantiating a new decoder\n   * when this case occurs.\n   * <p>\n   * See [Internal: b/8578467, b/23361053].\n   *\n   * @param name The name of the decoder.\n   * @return True if the decoder is known to behave incorrectly if flushed after receiving an input\n   *     buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise.\n   */\n  private static boolean codecNeedsEosFlushWorkaround(String name) {\n    return (Util.SDK_INT <= 23 && \"OMX.google.vorbis.decoder\".equals(name))\n        || (Util.SDK_INT <= 19\n            && (\"hb2000\".equals(Util.DEVICE) || \"stvm8\".equals(Util.DEVICE))\n            && (\"OMX.amlogic.avc.decoder.awesome\".equals(name)\n                || \"OMX.amlogic.avc.decoder.awesome.secure\".equals(name)));\n  }\n\n  /**\n   * Returns whether the decoder may throw an {@link IllegalStateException} from\n   * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or\n   * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input\n   * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.\n   * <p>\n   * See [Internal: b/17933838].\n   *\n   * @param name The name of the decoder.\n   * @return True if the decoder may throw an exception after receiving an end-of-stream buffer.\n   */\n  private static boolean codecNeedsEosOutputExceptionWorkaround(String name) {\n    return Util.SDK_INT == 21 && \"OMX.google.aac.decoder\".equals(name);\n  }\n\n  /**\n   * Returns whether the decoder is known to set the number of audio channels in the output {@link\n   * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single\n   * channel.\n   *\n   * <p>If true is returned then we explicitly override the number of channels in the output {@link\n   * Format}, setting it to 1.\n   *\n   * @param name The decoder name.\n   * @param format The input {@link Format}.\n   * @return True if the decoder is known to set the number of audio channels in the output {@link\n   *     Format} to 2 for the given input {@link Format}, whilst only actually outputting a single\n   *     channel. False otherwise.\n   */\n  private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) {\n    return Util.SDK_INT <= 18 && format.channelCount == 1\n        && \"OMX.MTK.AUDIO.DECODER.MP3\".equals(name);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.mediacodec;\n\nimport android.media.MediaCodec;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport java.util.List;\n\n/**\n * Selector of {@link MediaCodec} instances.\n */\npublic interface MediaCodecSelector {\n\n  /**\n   * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for\n   * the given format.\n   */\n  MediaCodecSelector DEFAULT =\n      new MediaCodecSelector() {\n        @Override\n        public List<MediaCodecInfo> getDecoderInfos(\n            String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder)\n            throws DecoderQueryException {\n          return MediaCodecUtil.getDecoderInfos(\n              mimeType, requiresSecureDecoder, requiresTunnelingDecoder);\n        }\n\n        @Override\n        @Nullable\n        public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {\n          return MediaCodecUtil.getPassthroughDecoderInfo();\n        }\n      };\n\n  /**\n   * Returns a list of decoders that can decode media in the specified MIME type, in priority order.\n   *\n   * @param mimeType The MIME type for which a decoder is required.\n   * @param requiresSecureDecoder Whether a secure decoder is required.\n   * @param requiresTunnelingDecoder Whether a tunneling decoder is required.\n   * @return An unmodifiable list of {@link MediaCodecInfo}s corresponding to decoders. May be\n   *     empty.\n   * @throws DecoderQueryException Thrown if there was an error querying decoders.\n   */\n  List<MediaCodecInfo> getDecoderInfos(\n          String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder)\n      throws DecoderQueryException;\n\n  /**\n   * Selects a decoder to instantiate for audio passthrough.\n   *\n   * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.\n   * @throws DecoderQueryException Thrown if there was an error querying decoders.\n   */\n  @Nullable\n  MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.mediacodec;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.media.MediaCodecInfo.CodecCapabilities;\nimport android.media.MediaCodecInfo.CodecProfileLevel;\nimport android.media.MediaCodecList;\nimport android.text.TextUtils;\nimport android.util.Pair;\nimport android.util.SparseIntArray;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\n\n/**\n * A utility class for querying the available codecs.\n */\n@SuppressLint(\"InlinedApi\")\npublic final class MediaCodecUtil {\n\n  /**\n   * Thrown when an error occurs querying the device for its underlying media capabilities.\n   * <p>\n   * Such failures are not expected in normal operation and are normally temporary (e.g. if the\n   * mediaserver process has crashed and is yet to restart).\n   */\n  public static class DecoderQueryException extends Exception {\n\n    private DecoderQueryException(Throwable cause) {\n      super(\"Failed to query underlying media codecs\", cause);\n    }\n\n  }\n\n  private static final String TAG = \"MediaCodecUtil\";\n  private static final Pattern PROFILE_PATTERN = Pattern.compile(\"^\\\\D?(\\\\d+)$\");\n\n  private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();\n\n  // Codecs to constant mappings.\n  // AVC.\n  private static final SparseIntArray AVC_PROFILE_NUMBER_TO_CONST;\n  private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST;\n  private static final String CODEC_ID_AVC1 = \"avc1\";\n  private static final String CODEC_ID_AVC2 = \"avc2\";\n  // VP9\n  private static final SparseIntArray VP9_PROFILE_NUMBER_TO_CONST;\n  private static final SparseIntArray VP9_LEVEL_NUMBER_TO_CONST;\n  private static final String CODEC_ID_VP09 = \"vp09\";\n  // HEVC.\n  private static final Map<String, Integer> HEVC_CODEC_STRING_TO_PROFILE_LEVEL;\n  private static final String CODEC_ID_HEV1 = \"hev1\";\n  private static final String CODEC_ID_HVC1 = \"hvc1\";\n  // Dolby Vision.\n  private static final Map<String, Integer> DOLBY_VISION_STRING_TO_PROFILE;\n  private static final Map<String, Integer> DOLBY_VISION_STRING_TO_LEVEL;\n  // AV1.\n  private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST;\n  private static final String CODEC_ID_AV01 = \"av01\";\n  // MP4A AAC.\n  private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE;\n  private static final String CODEC_ID_MP4A = \"mp4a\";\n\n  // Lazily initialized.\n  private static int maxH264DecodableFrameSize = -1;\n\n  private MediaCodecUtil() {}\n\n  /**\n   * Optional call to warm the codec cache for a given mime type.\n   *\n   * <p>Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean,\n   * boolean)} and {@link #getDecoderInfos(String, boolean, boolean)}.\n   *\n   * @param mimeType The mime type.\n   * @param secure Whether the decoder is required to support secure decryption. Always pass false\n   *     unless secure decryption really is required.\n   * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless\n   *     tunneling really is required.\n   */\n  public static void warmDecoderInfoCache(String mimeType, boolean secure, boolean tunneling) {\n    try {\n      getDecoderInfos(mimeType, secure, tunneling);\n    } catch (DecoderQueryException e) {\n      // Codec warming is best effort, so we can swallow the exception.\n      Log.e(TAG, \"Codec warming failed\", e);\n    }\n  }\n\n  /**\n   * Returns information about a decoder suitable for audio passthrough.\n   *\n   * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.\n   * @throws DecoderQueryException If there was an error querying the available decoders.\n   */\n  @Nullable\n  public static MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {\n    @Nullable\n    MediaCodecInfo decoderInfo =\n        getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false);\n    return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name);\n  }\n\n  /**\n   * Returns information about the preferred decoder for a given mime type.\n   *\n   * @param mimeType The MIME type.\n   * @param secure Whether the decoder is required to support secure decryption. Always pass false\n   *     unless secure decryption really is required.\n   * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless\n   *     tunneling really is required.\n   * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.\n   * @throws DecoderQueryException If there was an error querying the available decoders.\n   */\n  @Nullable\n  public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling)\n      throws DecoderQueryException {\n    List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure, tunneling);\n    return decoderInfos.isEmpty() ? null : decoderInfos.get(0);\n  }\n\n  /**\n   * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link\n   * MediaCodecList}.\n   *\n   * @param mimeType The MIME type.\n   * @param secure Whether the decoder is required to support secure decryption. Always pass false\n   *     unless secure decryption really is required.\n   * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless\n   *     tunneling really is required.\n   * @return An unmodifiable list of all {@link MediaCodecInfo}s for the given mime type, in the\n   *     order given by {@link MediaCodecList}.\n   * @throws DecoderQueryException If there was an error querying the available decoders.\n   */\n  public static synchronized List<MediaCodecInfo> getDecoderInfos(\n      String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException {\n    CodecKey key = new CodecKey(mimeType, secure, tunneling);\n    @Nullable List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);\n    if (cachedDecoderInfos != null) {\n      return cachedDecoderInfos;\n    }\n    MediaCodecListCompat mediaCodecList =\n        Util.SDK_INT >= 21\n            ? new MediaCodecListCompatV21(secure, tunneling)\n            : new MediaCodecListCompatV16();\n    ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList);\n    if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {\n      // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the\n      // legacy path. We also try this path on API levels 22 and 23 as a defensive measure.\n      mediaCodecList = new MediaCodecListCompatV16();\n      decoderInfos = getDecoderInfosInternal(key, mediaCodecList);\n      if (!decoderInfos.isEmpty()) {\n        Log.w(TAG, \"MediaCodecList API didn't list secure decoder for: \" + mimeType\n            + \". Assuming: \" + decoderInfos.get(0).name);\n      }\n    }\n    applyWorkarounds(mimeType, decoderInfos);\n    List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);\n    decoderInfosCache.put(key, unmodifiableDecoderInfos);\n    return unmodifiableDecoderInfos;\n  }\n\n  /**\n   * Returns a copy of the provided decoder list sorted such that decoders with format support are\n   * listed first. The returned list is modifiable for convenience.\n   */\n  @CheckResult\n  public static List<MediaCodecInfo> getDecoderInfosSortedByFormatSupport(\n      List<MediaCodecInfo> decoderInfos, Format format) {\n    decoderInfos = new ArrayList<>(decoderInfos);\n    sortByScore(\n        decoderInfos,\n        decoderInfo -> {\n          try {\n            return decoderInfo.isFormatSupported(format) ? 1 : 0;\n          } catch (DecoderQueryException e) {\n            return -1;\n          }\n        });\n    return decoderInfos;\n  }\n\n  /**\n   * Returns the maximum frame size supported by the default H264 decoder.\n   *\n   * @return The maximum frame size for an H264 stream that can be decoded on the device.\n   */\n  public static int maxH264DecodableFrameSize() throws DecoderQueryException {\n    if (maxH264DecodableFrameSize == -1) {\n      int result = 0;\n      @Nullable\n      MediaCodecInfo decoderInfo =\n          getDecoderInfo(MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false);\n      if (decoderInfo != null) {\n        for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {\n          result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);\n        }\n        // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are\n        // the levels mandated by the Android CDD.\n        result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));\n      }\n      maxH264DecodableFrameSize = result;\n    }\n    return maxH264DecodableFrameSize;\n  }\n\n  /**\n   * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec\n   * description string (as defined by RFC 6381) of the given format.\n   *\n   * @param format Media format with a codec description string, as defined by RFC 6381.\n   * @return A pair (profile constant, level constant) if the codec of the {@code format} is\n   *     well-formed and recognized, or null otherwise.\n   */\n  @Nullable\n  public static Pair<Integer, Integer> getCodecProfileAndLevel(Format format) {\n    if (format.codecs == null) {\n      return null;\n    }\n    String[] parts = format.codecs.split(\"\\\\.\");\n    // Dolby Vision can use DV, AVC or HEVC codec IDs, so check the MIME type first.\n    if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) {\n      return getDolbyVisionProfileAndLevel(format.codecs, parts);\n    }\n    switch (parts[0]) {\n      case CODEC_ID_AVC1:\n      case CODEC_ID_AVC2:\n        return getAvcProfileAndLevel(format.codecs, parts);\n      case CODEC_ID_VP09:\n        return getVp9ProfileAndLevel(format.codecs, parts);\n      case CODEC_ID_HEV1:\n      case CODEC_ID_HVC1:\n        return getHevcProfileAndLevel(format.codecs, parts);\n      case CODEC_ID_AV01:\n        return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo);\n      case CODEC_ID_MP4A:\n        return getAacCodecProfileAndLevel(format.codecs, parts);\n      default:\n        return null;\n    }\n  }\n\n  // Internal methods.\n\n  /**\n   * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by\n   * {@code mediaCodecList}.\n   *\n   * @param key The codec key.\n   * @param mediaCodecList The codec list.\n   * @return The codec information for usable codecs matching the specified key.\n   * @throws DecoderQueryException If there was an error querying the available decoders.\n   */\n  private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(\n      CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {\n    try {\n      ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();\n      String mimeType = key.mimeType;\n      int numberOfCodecs = mediaCodecList.getCodecCount();\n      boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();\n      // Note: MediaCodecList is sorted by the framework such that the best decoders come first.\n      for (int i = 0; i < numberOfCodecs; i++) {\n        android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);\n        String name = codecInfo.getName();\n        @Nullable\n        String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType);\n        if (codecMimeType == null) {\n          continue;\n        }\n        try {\n          CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType);\n          boolean tunnelingSupported =\n              mediaCodecList.isFeatureSupported(\n                  CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);\n          boolean tunnelingRequired =\n              mediaCodecList.isFeatureRequired(\n                  CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);\n          if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) {\n            continue;\n          }\n          boolean secureSupported =\n              mediaCodecList.isFeatureSupported(\n                  CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);\n          boolean secureRequired =\n              mediaCodecList.isFeatureRequired(\n                  CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);\n          if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) {\n            continue;\n          }\n          boolean hardwareAccelerated = isHardwareAccelerated(codecInfo);\n          boolean softwareOnly = isSoftwareOnly(codecInfo);\n          boolean vendor = isVendor(codecInfo);\n          boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(name);\n          if ((secureDecodersExplicit && key.secure == secureSupported)\n              || (!secureDecodersExplicit && !key.secure)) {\n            decoderInfos.add(\n                MediaCodecInfo.newInstance(\n                    name,\n                    mimeType,\n                    codecMimeType,\n                    capabilities,\n                    hardwareAccelerated,\n                    softwareOnly,\n                    vendor,\n                    forceDisableAdaptive,\n                    /* forceSecure= */ false));\n          } else if (!secureDecodersExplicit && secureSupported) {\n            decoderInfos.add(\n                MediaCodecInfo.newInstance(\n                    name + \".secure\",\n                    mimeType,\n                    codecMimeType,\n                    capabilities,\n                    hardwareAccelerated,\n                    softwareOnly,\n                    vendor,\n                    forceDisableAdaptive,\n                    /* forceSecure= */ true));\n            // It only makes sense to have one synthesized secure decoder, return immediately.\n            return decoderInfos;\n          }\n        } catch (Exception e) {\n          if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) {\n            // Suppress error querying secondary codec capabilities up to API level 23.\n            Log.e(TAG, \"Skipping codec \" + name + \" (failed to query capabilities)\");\n          } else {\n            // Rethrow error querying primary codec capabilities, or secondary codec\n            // capabilities if API level is greater than 23.\n            Log.e(TAG, \"Failed to query codec \" + name + \" (\" + codecMimeType + \")\");\n            throw e;\n          }\n        }\n      }\n      return decoderInfos;\n    } catch (Exception e) {\n      // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException\n      // or an IllegalArgumentException here.\n      throw new DecoderQueryException(e);\n    }\n  }\n\n  /**\n   * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if\n   * the codec can't be used.\n   *\n   * @param info The codec information.\n   * @param name The name of the codec\n   * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.\n   * @param mimeType The MIME type.\n   * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if\n   *     the codec can't be used. If non-null, the returned type will be equal to {@code mimeType}\n   *     except in cases where the codec is known to use a non-standard MIME type alias.\n   */\n  @Nullable\n  private static String getCodecMimeType(\n      android.media.MediaCodecInfo info,\n      String name,\n      boolean secureDecodersExplicit,\n      String mimeType) {\n    if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) {\n      return null;\n    }\n\n    String[] supportedTypes = info.getSupportedTypes();\n    for (String supportedType : supportedTypes) {\n      if (supportedType.equalsIgnoreCase(mimeType)) {\n        return supportedType;\n      }\n    }\n\n    if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) {\n      // Handle decoders that declare support for DV via MIME types that aren't\n      // video/dolby-vision.\n      if (\"OMX.MS.HEVCDV.Decoder\".equals(name)) {\n        return \"video/hevcdv\";\n      } else if (\"OMX.RTK.video.decoder\".equals(name)\n          || \"OMX.realtek.video.decoder.tunneled\".equals(name)) {\n        return \"video/dv_hevc\";\n      }\n    } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && \"OMX.lge.alac.decoder\".equals(name)) {\n      return \"audio/x-lg-alac\";\n    } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && \"OMX.lge.flac.decoder\".equals(name)) {\n      return \"audio/x-lg-flac\";\n    }\n\n    return null;\n  }\n\n  /**\n   * Returns whether the specified codec is usable for decoding on the current device.\n   *\n   * @param info The codec information.\n   * @param name The name of the codec\n   * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.\n   * @param mimeType The MIME type.\n   * @return Whether the specified codec is usable for decoding on the current device.\n   */\n  private static boolean isCodecUsableDecoder(\n      android.media.MediaCodecInfo info,\n      String name,\n      boolean secureDecodersExplicit,\n      String mimeType) {\n    if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(\".secure\"))) {\n      return false;\n    }\n\n    // Work around broken audio decoders.\n    if (Util.SDK_INT < 21\n        && (\"CIPAACDecoder\".equals(name)\n            || \"CIPMP3Decoder\".equals(name)\n            || \"CIPVorbisDecoder\".equals(name)\n            || \"CIPAMRNBDecoder\".equals(name)\n            || \"AACDecoder\".equals(name)\n            || \"MP3Decoder\".equals(name))) {\n      return false;\n    }\n\n    // Work around https://github.com/google/ExoPlayer/issues/1528 and\n    // https://github.com/google/ExoPlayer/issues/3171.\n    if (Util.SDK_INT < 18\n        && \"OMX.MTK.AUDIO.DECODER.AAC\".equals(name)\n        && (\"a70\".equals(Util.DEVICE)\n            || (\"Xiaomi\".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith(\"HM\")))) {\n      return false;\n    }\n\n    // Work around an issue where querying/creating a particular MP3 decoder on some devices on\n    // platform API version 16 fails.\n    if (Util.SDK_INT == 16\n        && \"OMX.qcom.audio.decoder.mp3\".equals(name)\n        && (\"dlxu\".equals(Util.DEVICE) // HTC Butterfly\n            || \"protou\".equals(Util.DEVICE) // HTC Desire X\n            || \"ville\".equals(Util.DEVICE) // HTC One S\n            || \"villeplus\".equals(Util.DEVICE)\n            || \"villec2\".equals(Util.DEVICE)\n            || Util.DEVICE.startsWith(\"gee\") // LGE Optimus G\n            || \"C6602\".equals(Util.DEVICE) // Sony Xperia Z\n            || \"C6603\".equals(Util.DEVICE)\n            || \"C6606\".equals(Util.DEVICE)\n            || \"C6616\".equals(Util.DEVICE)\n            || \"L36h\".equals(Util.DEVICE)\n            || \"SO-02E\".equals(Util.DEVICE))) {\n      return false;\n    }\n\n    // Work around an issue where large timestamps are not propagated correctly.\n    if (Util.SDK_INT == 16\n        && \"OMX.qcom.audio.decoder.aac\".equals(name)\n        && (\"C1504\".equals(Util.DEVICE) // Sony Xperia E\n            || \"C1505\".equals(Util.DEVICE)\n            || \"C1604\".equals(Util.DEVICE) // Sony Xperia E dual\n            || \"C1605\".equals(Util.DEVICE))) {\n      return false;\n    }\n\n    // Work around https://github.com/google/ExoPlayer/issues/3249.\n    if (Util.SDK_INT < 24\n        && (\"OMX.SEC.aac.dec\".equals(name) || \"OMX.Exynos.AAC.Decoder\".equals(name))\n        && \"samsung\".equals(Util.MANUFACTURER)\n        && (Util.DEVICE.startsWith(\"zeroflte\") // Galaxy S6\n            || Util.DEVICE.startsWith(\"zerolte\") // Galaxy S6 Edge\n            || Util.DEVICE.startsWith(\"zenlte\") // Galaxy S6 Edge+\n            || \"SC-05G\".equals(Util.DEVICE) // Galaxy S6\n            || \"marinelteatt\".equals(Util.DEVICE) // Galaxy S6 Active\n            || \"404SC\".equals(Util.DEVICE) // Galaxy S6 Edge\n            || \"SC-04G\".equals(Util.DEVICE)\n            || \"SCV31\".equals(Util.DEVICE))) {\n      return false;\n    }\n\n    // Work around https://github.com/google/ExoPlayer/issues/548.\n    // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video.\n    if (Util.SDK_INT <= 19\n        && \"OMX.SEC.vp8.dec\".equals(name)\n        && \"samsung\".equals(Util.MANUFACTURER)\n        && (Util.DEVICE.startsWith(\"d2\")\n            || Util.DEVICE.startsWith(\"serrano\")\n            || Util.DEVICE.startsWith(\"jflte\")\n            || Util.DEVICE.startsWith(\"santos\")\n            || Util.DEVICE.startsWith(\"t0\"))) {\n      return false;\n    }\n\n    // VP8 decoder on Samsung Galaxy S4 cannot be queried.\n    if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith(\"jflte\")\n        && \"OMX.qcom.video.decoder.vp8\".equals(name)) {\n      return false;\n    }\n\n    // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].\n    if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && \"OMX.MTK.AUDIO.DECODER.DSPAC3\".equals(name)) {\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the\n   * platform.\n   *\n   * @param mimeType The MIME type of input media.\n   * @param decoderInfos The list to modify.\n   */\n  private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) {\n    if (MimeTypes.AUDIO_RAW.equals(mimeType)) {\n      if (Util.SDK_INT < 26\n          && Util.DEVICE.equals(\"R9\")\n          && decoderInfos.size() == 1\n          && decoderInfos.get(0).name.equals(\"OMX.MTK.AUDIO.DECODER.RAW\")) {\n        // This device does not list a generic raw audio decoder, yet it can be instantiated by\n        // name. See <a href=\"https://github.com/google/ExoPlayer/issues/5782\">Issue #5782</a>.\n        decoderInfos.add(\n            MediaCodecInfo.newInstance(\n                /* name= */ \"OMX.google.raw.decoder\",\n                /* mimeType= */ MimeTypes.AUDIO_RAW,\n                /* codecMimeType= */ MimeTypes.AUDIO_RAW,\n                /* capabilities= */ null,\n                /* hardwareAccelerated= */ false,\n                /* softwareOnly= */ true,\n                /* vendor= */ false,\n                /* forceDisableAdaptive= */ false,\n                /* forceSecure= */ false));\n      }\n      // Work around inconsistent raw audio decoding behavior across different devices.\n      sortByScore(\n          decoderInfos,\n          decoderInfo -> {\n            String name = decoderInfo.name;\n            if (name.startsWith(\"OMX.google\") || name.startsWith(\"c2.android\")) {\n              // Prefer generic decoders over ones provided by the device.\n              return 1;\n            }\n            if (Util.SDK_INT < 26 && name.equals(\"OMX.MTK.AUDIO.DECODER.RAW\")) {\n              // This decoder may modify the audio, so any other compatible decoders take\n              // precedence. See [Internal: b/62337687].\n              return -1;\n            }\n            return 0;\n          });\n    } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) {\n      String firstCodecName = decoderInfos.get(0).name;\n      if (\"OMX.SEC.mp3.dec\".equals(firstCodecName)\n          || \"OMX.SEC.MP3.Decoder\".equals(firstCodecName)\n          || \"OMX.brcm.audio.mp3.decoder\".equals(firstCodecName)) {\n        // Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and\n        // OMX.brcm.audio.mp3.decoder on older devices. See:\n        // https://github.com/google/ExoPlayer/issues/398 and\n        // https://github.com/google/ExoPlayer/issues/4519.\n        sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith(\"OMX.google\") ? 1 : 0);\n      }\n    }\n  }\n\n  /**\n   * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+,\n   * or a best-effort approximation for lower levels.\n   */\n  private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) {\n    if (Util.SDK_INT >= 29) {\n      return isHardwareAcceleratedV29(codecInfo);\n    }\n    // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true.\n    // However, we assume this to be true as an approximation.\n    return !isSoftwareOnly(codecInfo);\n  }\n\n  @TargetApi(29)\n  private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) {\n    return codecInfo.isHardwareAccelerated();\n  }\n\n  /**\n   * The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a\n   * best-effort approximation for lower levels.\n   */\n  private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) {\n    if (Util.SDK_INT >= 29) {\n      return isSoftwareOnlyV29(codecInfo);\n    }\n    String codecName = Util.toLowerInvariant(codecInfo.getName());\n    if (codecName.startsWith(\"arc.\")) { // App Runtime for Chrome (ARC) codecs\n      return false;\n    }\n    return codecName.startsWith(\"omx.google.\")\n        || codecName.startsWith(\"omx.ffmpeg.\")\n        || (codecName.startsWith(\"omx.sec.\") && codecName.contains(\".sw.\"))\n        || codecName.equals(\"omx.qcom.video.decoder.hevcswvdec\")\n        || codecName.startsWith(\"c2.android.\")\n        || codecName.startsWith(\"c2.google.\")\n        || (!codecName.startsWith(\"omx.\") && !codecName.startsWith(\"c2.\"));\n  }\n\n  @TargetApi(29)\n  private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) {\n    return codecInfo.isSoftwareOnly();\n  }\n\n  /**\n   * The result of {@link android.media.MediaCodecInfo#isVendor()} for API levels 29+, or a\n   * best-effort approximation for lower levels.\n   */\n  private static boolean isVendor(android.media.MediaCodecInfo codecInfo) {\n    if (Util.SDK_INT >= 29) {\n      return isVendorV29(codecInfo);\n    }\n    String codecName = Util.toLowerInvariant(codecInfo.getName());\n    return !codecName.startsWith(\"omx.google.\")\n        && !codecName.startsWith(\"c2.android.\")\n        && !codecName.startsWith(\"c2.google.\");\n  }\n\n  @TargetApi(29)\n  private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) {\n    return codecInfo.isVendor();\n  }\n\n  /**\n   * Returns whether the decoder is known to fail when adapting, despite advertising itself as an\n   * adaptive decoder.\n   *\n   * @param name The decoder name.\n   * @return True if the decoder is known to fail when adapting.\n   */\n  private static boolean codecNeedsDisableAdaptationWorkaround(String name) {\n    return Util.SDK_INT <= 22\n        && (\"ODROID-XU3\".equals(Util.MODEL) || \"Nexus 10\".equals(Util.MODEL))\n        && (\"OMX.Exynos.AVC.Decoder\".equals(name) || \"OMX.Exynos.AVC.Decoder.secure\".equals(name));\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getDolbyVisionProfileAndLevel(\n      String codec, String[] parts) {\n    if (parts.length < 3) {\n      // The codec has fewer parts than required by the Dolby Vision codec string format.\n      Log.w(TAG, \"Ignoring malformed Dolby Vision codec string: \" + codec);\n      return null;\n    }\n    // The profile_space gets ignored.\n    Matcher matcher = PROFILE_PATTERN.matcher(parts[1]);\n    if (!matcher.matches()) {\n      Log.w(TAG, \"Ignoring malformed Dolby Vision codec string: \" + codec);\n      return null;\n    }\n    @Nullable String profileString = matcher.group(1);\n    @Nullable Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString);\n    if (profile == null) {\n      Log.w(TAG, \"Unknown Dolby Vision profile string: \" + profileString);\n      return null;\n    }\n    String levelString = parts[2];\n    @Nullable Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString);\n    if (level == null) {\n      Log.w(TAG, \"Unknown Dolby Vision level string: \" + levelString);\n      return null;\n    }\n    return new Pair<>(profile, level);\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getHevcProfileAndLevel(String codec, String[] parts) {\n    if (parts.length < 4) {\n      // The codec has fewer parts than required by the HEVC codec string format.\n      Log.w(TAG, \"Ignoring malformed HEVC codec string: \" + codec);\n      return null;\n    }\n    // The profile_space gets ignored.\n    Matcher matcher = PROFILE_PATTERN.matcher(parts[1]);\n    if (!matcher.matches()) {\n      Log.w(TAG, \"Ignoring malformed HEVC codec string: \" + codec);\n      return null;\n    }\n    @Nullable String profileString = matcher.group(1);\n    int profile;\n    if (\"1\".equals(profileString)) {\n      profile = CodecProfileLevel.HEVCProfileMain;\n    } else if (\"2\".equals(profileString)) {\n      profile = CodecProfileLevel.HEVCProfileMain10;\n    } else {\n      Log.w(TAG, \"Unknown HEVC profile string: \" + profileString);\n      return null;\n    }\n    @Nullable String levelString = parts[3];\n    @Nullable Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString);\n    if (level == null) {\n      Log.w(TAG, \"Unknown HEVC level string: \" + levelString);\n      return null;\n    }\n    return new Pair<>(profile, level);\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getAvcProfileAndLevel(String codec, String[] parts) {\n    if (parts.length < 2) {\n      // The codec has fewer parts than required by the AVC codec string format.\n      Log.w(TAG, \"Ignoring malformed AVC codec string: \" + codec);\n      return null;\n    }\n    int profileInteger;\n    int levelInteger;\n    try {\n      if (parts[1].length() == 6) {\n        // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal.\n        profileInteger = Integer.parseInt(parts[1].substring(0, 2), 16);\n        levelInteger = Integer.parseInt(parts[1].substring(4), 16);\n      } else if (parts.length >= 3) {\n        // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal.\n        profileInteger = Integer.parseInt(parts[1]);\n        levelInteger = Integer.parseInt(parts[2]);\n      } else {\n        // We don't recognize the format.\n        Log.w(TAG, \"Ignoring malformed AVC codec string: \" + codec);\n        return null;\n      }\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed AVC codec string: \" + codec);\n      return null;\n    }\n\n    int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);\n    if (profile == -1) {\n      Log.w(TAG, \"Unknown AVC profile: \" + profileInteger);\n      return null;\n    }\n    int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);\n    if (level == -1) {\n      Log.w(TAG, \"Unknown AVC level: \" + levelInteger);\n      return null;\n    }\n    return new Pair<>(profile, level);\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getVp9ProfileAndLevel(String codec, String[] parts) {\n    if (parts.length < 3) {\n      Log.w(TAG, \"Ignoring malformed VP9 codec string: \" + codec);\n      return null;\n    }\n    int profileInteger;\n    int levelInteger;\n    try {\n      profileInteger = Integer.parseInt(parts[1]);\n      levelInteger = Integer.parseInt(parts[2]);\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed VP9 codec string: \" + codec);\n      return null;\n    }\n\n    int profile = VP9_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);\n    if (profile == -1) {\n      Log.w(TAG, \"Unknown VP9 profile: \" + profileInteger);\n      return null;\n    }\n    int level = VP9_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);\n    if (level == -1) {\n      Log.w(TAG, \"Unknown VP9 level: \" + levelInteger);\n      return null;\n    }\n    return new Pair<>(profile, level);\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getAv1ProfileAndLevel(\n      String codec, String[] parts, @Nullable ColorInfo colorInfo) {\n    if (parts.length < 4) {\n      Log.w(TAG, \"Ignoring malformed AV1 codec string: \" + codec);\n      return null;\n    }\n    int profileInteger;\n    int levelInteger;\n    int bitDepthInteger;\n    try {\n      profileInteger = Integer.parseInt(parts[1]);\n      levelInteger = Integer.parseInt(parts[2].substring(0, 2));\n      bitDepthInteger = Integer.parseInt(parts[3]);\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed AV1 codec string: \" + codec);\n      return null;\n    }\n\n    if (profileInteger != 0) {\n      Log.w(TAG, \"Unknown AV1 profile: \" + profileInteger);\n      return null;\n    }\n    if (bitDepthInteger != 8 && bitDepthInteger != 10) {\n      Log.w(TAG, \"Unknown AV1 bit depth: \" + bitDepthInteger);\n      return null;\n    }\n    int profile;\n    if (bitDepthInteger == 8) {\n      profile = CodecProfileLevel.AV1ProfileMain8;\n    } else if (colorInfo != null\n        && (colorInfo.hdrStaticInfo != null\n            || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG\n            || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) {\n      profile = CodecProfileLevel.AV1ProfileMain10HDR10;\n    } else {\n      profile = CodecProfileLevel.AV1ProfileMain10;\n    }\n\n    int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);\n    if (level == -1) {\n      Log.w(TAG, \"Unknown AV1 level: \" + levelInteger);\n      return null;\n    }\n    return new Pair<>(profile, level);\n  }\n\n  /**\n   * Conversion values taken from ISO 14496-10 Table A-1.\n   *\n   * @param avcLevel one of CodecProfileLevel.AVCLevel* constants.\n   * @return maximum frame size that can be decoded by a decoder with the specified avc level\n   *     (or {@code -1} if the level is not recognized)\n   */\n  private static int avcLevelToMaxFrameSize(int avcLevel) {\n    switch (avcLevel) {\n      case CodecProfileLevel.AVCLevel1:\n      case CodecProfileLevel.AVCLevel1b:\n        return 99 * 16 * 16;\n      case CodecProfileLevel.AVCLevel12:\n      case CodecProfileLevel.AVCLevel13:\n      case CodecProfileLevel.AVCLevel2:\n        return 396 * 16 * 16;\n      case CodecProfileLevel.AVCLevel21:\n        return 792 * 16 * 16;\n      case CodecProfileLevel.AVCLevel22:\n      case CodecProfileLevel.AVCLevel3:\n        return 1620 * 16 * 16;\n      case CodecProfileLevel.AVCLevel31:\n        return 3600 * 16 * 16;\n      case CodecProfileLevel.AVCLevel32:\n        return 5120 * 16 * 16;\n      case CodecProfileLevel.AVCLevel4:\n      case CodecProfileLevel.AVCLevel41:\n        return 8192 * 16 * 16;\n      case CodecProfileLevel.AVCLevel42:\n        return 8704 * 16 * 16;\n      case CodecProfileLevel.AVCLevel5:\n        return 22080 * 16 * 16;\n      case CodecProfileLevel.AVCLevel51:\n      case CodecProfileLevel.AVCLevel52:\n        return 36864 * 16 * 16;\n      default:\n        return -1;\n    }\n  }\n\n  @Nullable\n  private static Pair<Integer, Integer> getAacCodecProfileAndLevel(String codec, String[] parts) {\n    if (parts.length != 3) {\n      Log.w(TAG, \"Ignoring malformed MP4A codec string: \" + codec);\n      return null;\n    }\n    try {\n      // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1).\n      int objectTypeIndication = Integer.parseInt(parts[1], 16);\n      @Nullable String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication);\n      if (MimeTypes.AUDIO_AAC.equals(mimeType)) {\n        // For MPEG-4 audio this is followed by an audio object type indication as a decimal number.\n        int audioObjectTypeIndication = Integer.parseInt(parts[2]);\n        int profile = MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.get(audioObjectTypeIndication, -1);\n        if (profile != -1) {\n          // Level is set to zero in AAC decoder CodecProfileLevels.\n          return new Pair<>(profile, 0);\n        }\n      }\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed MP4A codec string: \" + codec);\n    }\n    return null;\n  }\n\n  /** Stably sorts the provided {@code list} in-place, in order of decreasing score. */\n  private static <T> void sortByScore(List<T> list, ScoreProvider<T> scoreProvider) {\n    Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a));\n  }\n\n  /** Interface for providers of item scores. */\n  private interface ScoreProvider<T> {\n    /** Returns the score of the provided item. */\n    int getScore(T t);\n  }\n\n  private interface MediaCodecListCompat {\n\n    /**\n     * The number of codecs in the list.\n     */\n    int getCodecCount();\n\n    /**\n     * The info at the specified index in the list.\n     *\n     * @param index The index.\n     */\n    android.media.MediaCodecInfo getCodecInfoAt(int index);\n\n    /**\n     * Returns whether secure decoders are explicitly listed, if present.\n     */\n    boolean secureDecodersExplicit();\n\n    /** Whether the specified {@link CodecCapabilities} {@code feature} is supported. */\n    boolean isFeatureSupported(String feature, String mimeType, CodecCapabilities capabilities);\n\n    /** Whether the specified {@link CodecCapabilities} {@code feature} is required. */\n    boolean isFeatureRequired(String feature, String mimeType, CodecCapabilities capabilities);\n  }\n\n  @TargetApi(21)\n  private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {\n\n    private final int codecKind;\n\n    @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos;\n\n    // the constructor does not initialize fields: mediaCodecInfos\n    @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n    public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) {\n      codecKind =\n          includeSecure || includeTunneling\n              ? MediaCodecList.ALL_CODECS\n              : MediaCodecList.REGULAR_CODECS;\n    }\n\n    @Override\n    public int getCodecCount() {\n      ensureMediaCodecInfosInitialized();\n      return mediaCodecInfos.length;\n    }\n\n    // incompatible types in return.\n    @SuppressWarnings(\"nullness:return.type.incompatible\")\n    @Override\n    public android.media.MediaCodecInfo getCodecInfoAt(int index) {\n      ensureMediaCodecInfosInitialized();\n      return mediaCodecInfos[index];\n    }\n\n    @Override\n    public boolean secureDecodersExplicit() {\n      return true;\n    }\n\n    @Override\n    public boolean isFeatureSupported(\n        String feature, String mimeType, CodecCapabilities capabilities) {\n      return capabilities.isFeatureSupported(feature);\n    }\n\n    @Override\n    public boolean isFeatureRequired(\n        String feature, String mimeType, CodecCapabilities capabilities) {\n      return capabilities.isFeatureRequired(feature);\n    }\n\n    @EnsuresNonNull({\"mediaCodecInfos\"})\n    private void ensureMediaCodecInfosInitialized() {\n      if (mediaCodecInfos == null) {\n        mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();\n      }\n    }\n\n  }\n\n  private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {\n\n    @Override\n    public int getCodecCount() {\n      return MediaCodecList.getCodecCount();\n    }\n\n    @Override\n    public android.media.MediaCodecInfo getCodecInfoAt(int index) {\n      return MediaCodecList.getCodecInfoAt(index);\n    }\n\n    @Override\n    public boolean secureDecodersExplicit() {\n      return false;\n    }\n\n    @Override\n    public boolean isFeatureSupported(\n        String feature, String mimeType, CodecCapabilities capabilities) {\n      // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure\n      // H264 decoder exists.\n      return CodecCapabilities.FEATURE_SecurePlayback.equals(feature)\n          && MimeTypes.VIDEO_H264.equals(mimeType);\n    }\n\n    @Override\n    public boolean isFeatureRequired(\n        String feature, String mimeType, CodecCapabilities capabilities) {\n      return false;\n    }\n\n  }\n\n  private static final class CodecKey {\n\n    public final String mimeType;\n    public final boolean secure;\n    public final boolean tunneling;\n\n    public CodecKey(String mimeType, boolean secure, boolean tunneling) {\n      this.mimeType = mimeType;\n      this.secure = secure;\n      this.tunneling = tunneling;\n    }\n\n    @Override\n    public int hashCode() {\n      final int prime = 31;\n      int result = 1;\n      result = prime * result + mimeType.hashCode();\n      result = prime * result + (secure ? 1231 : 1237);\n      result = prime * result + (tunneling ? 1231 : 1237);\n      return result;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || obj.getClass() != CodecKey.class) {\n        return false;\n      }\n      CodecKey other = (CodecKey) obj;\n      return TextUtils.equals(mimeType, other.mimeType)\n          && secure == other.secure\n          && tunneling == other.tunneling;\n    }\n\n  }\n\n  static {\n    AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();\n    AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);\n    AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain);\n    AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended);\n    AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh);\n    AVC_PROFILE_NUMBER_TO_CONST.put(110, CodecProfileLevel.AVCProfileHigh10);\n    AVC_PROFILE_NUMBER_TO_CONST.put(122, CodecProfileLevel.AVCProfileHigh422);\n    AVC_PROFILE_NUMBER_TO_CONST.put(244, CodecProfileLevel.AVCProfileHigh444);\n\n    AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray();\n    AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1);\n    // TODO: Find int for CodecProfileLevel.AVCLevel1b.\n    AVC_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AVCLevel11);\n    AVC_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AVCLevel12);\n    AVC_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AVCLevel13);\n    AVC_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AVCLevel2);\n    AVC_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AVCLevel21);\n    AVC_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AVCLevel22);\n    AVC_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.AVCLevel3);\n    AVC_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.AVCLevel31);\n    AVC_LEVEL_NUMBER_TO_CONST.put(32, CodecProfileLevel.AVCLevel32);\n    AVC_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.AVCLevel4);\n    AVC_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.AVCLevel41);\n    AVC_LEVEL_NUMBER_TO_CONST.put(42, CodecProfileLevel.AVCLevel42);\n    AVC_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.AVCLevel5);\n    AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51);\n    AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52);\n\n    VP9_PROFILE_NUMBER_TO_CONST = new SparseIntArray();\n    VP9_PROFILE_NUMBER_TO_CONST.put(0, CodecProfileLevel.VP9Profile0);\n    VP9_PROFILE_NUMBER_TO_CONST.put(1, CodecProfileLevel.VP9Profile1);\n    VP9_PROFILE_NUMBER_TO_CONST.put(2, CodecProfileLevel.VP9Profile2);\n    VP9_PROFILE_NUMBER_TO_CONST.put(3, CodecProfileLevel.VP9Profile3);\n    VP9_LEVEL_NUMBER_TO_CONST = new SparseIntArray();\n    VP9_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.VP9Level1);\n    VP9_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.VP9Level11);\n    VP9_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.VP9Level2);\n    VP9_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.VP9Level21);\n    VP9_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.VP9Level3);\n    VP9_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.VP9Level31);\n    VP9_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.VP9Level4);\n    VP9_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.VP9Level41);\n    VP9_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.VP9Level5);\n    VP9_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.VP9Level51);\n    VP9_LEVEL_NUMBER_TO_CONST.put(60, CodecProfileLevel.VP9Level6);\n    VP9_LEVEL_NUMBER_TO_CONST.put(61, CodecProfileLevel.VP9Level61);\n    VP9_LEVEL_NUMBER_TO_CONST.put(62, CodecProfileLevel.VP9Level62);\n\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>();\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L30\", CodecProfileLevel.HEVCMainTierLevel1);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L60\", CodecProfileLevel.HEVCMainTierLevel2);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L63\", CodecProfileLevel.HEVCMainTierLevel21);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L90\", CodecProfileLevel.HEVCMainTierLevel3);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L93\", CodecProfileLevel.HEVCMainTierLevel31);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L120\", CodecProfileLevel.HEVCMainTierLevel4);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L123\", CodecProfileLevel.HEVCMainTierLevel41);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L150\", CodecProfileLevel.HEVCMainTierLevel5);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L153\", CodecProfileLevel.HEVCMainTierLevel51);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L156\", CodecProfileLevel.HEVCMainTierLevel52);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L180\", CodecProfileLevel.HEVCMainTierLevel6);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L183\", CodecProfileLevel.HEVCMainTierLevel61);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"L186\", CodecProfileLevel.HEVCMainTierLevel62);\n\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H30\", CodecProfileLevel.HEVCHighTierLevel1);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H60\", CodecProfileLevel.HEVCHighTierLevel2);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H63\", CodecProfileLevel.HEVCHighTierLevel21);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H90\", CodecProfileLevel.HEVCHighTierLevel3);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H93\", CodecProfileLevel.HEVCHighTierLevel31);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H120\", CodecProfileLevel.HEVCHighTierLevel4);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H123\", CodecProfileLevel.HEVCHighTierLevel41);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H150\", CodecProfileLevel.HEVCHighTierLevel5);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H153\", CodecProfileLevel.HEVCHighTierLevel51);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H156\", CodecProfileLevel.HEVCHighTierLevel52);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H180\", CodecProfileLevel.HEVCHighTierLevel6);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H183\", CodecProfileLevel.HEVCHighTierLevel61);\n    HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put(\"H186\", CodecProfileLevel.HEVCHighTierLevel62);\n\n    DOLBY_VISION_STRING_TO_PROFILE = new HashMap<>();\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"00\", CodecProfileLevel.DolbyVisionProfileDvavPer);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"01\", CodecProfileLevel.DolbyVisionProfileDvavPen);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"02\", CodecProfileLevel.DolbyVisionProfileDvheDer);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"03\", CodecProfileLevel.DolbyVisionProfileDvheDen);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"04\", CodecProfileLevel.DolbyVisionProfileDvheDtr);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"05\", CodecProfileLevel.DolbyVisionProfileDvheStn);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"06\", CodecProfileLevel.DolbyVisionProfileDvheDth);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"07\", CodecProfileLevel.DolbyVisionProfileDvheDtb);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"08\", CodecProfileLevel.DolbyVisionProfileDvheSt);\n    DOLBY_VISION_STRING_TO_PROFILE.put(\"09\", CodecProfileLevel.DolbyVisionProfileDvavSe);\n\n    DOLBY_VISION_STRING_TO_LEVEL = new HashMap<>();\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"01\", CodecProfileLevel.DolbyVisionLevelHd24);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"02\", CodecProfileLevel.DolbyVisionLevelHd30);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"03\", CodecProfileLevel.DolbyVisionLevelFhd24);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"04\", CodecProfileLevel.DolbyVisionLevelFhd30);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"05\", CodecProfileLevel.DolbyVisionLevelFhd60);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"06\", CodecProfileLevel.DolbyVisionLevelUhd24);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"07\", CodecProfileLevel.DolbyVisionLevelUhd30);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"08\", CodecProfileLevel.DolbyVisionLevelUhd48);\n    DOLBY_VISION_STRING_TO_LEVEL.put(\"09\", CodecProfileLevel.DolbyVisionLevelUhd60);\n\n    // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for\n    // more information on mapping AV1 codec strings to levels.\n    AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray();\n    AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2);\n    AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21);\n    AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22);\n    AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23);\n    AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3);\n    AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31);\n    AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32);\n    AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33);\n    AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4);\n    AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41);\n    AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42);\n    AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43);\n    AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5);\n    AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51);\n    AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52);\n    AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53);\n    AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6);\n    AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61);\n    AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62);\n    AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63);\n    AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7);\n    AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71);\n    AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72);\n    AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73);\n\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray();\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(3, CodecProfileLevel.AACObjectSSR);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(4, CodecProfileLevel.AACObjectLTP);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(5, CodecProfileLevel.AACObjectHE);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(6, CodecProfileLevel.AACObjectScalable);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(17, CodecProfileLevel.AACObjectERLC);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(20, CodecProfileLevel.AACObjectERScalable);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(23, CodecProfileLevel.AACObjectLD);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(29, CodecProfileLevel.AACObjectHE_PS);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(39, CodecProfileLevel.AACObjectELD);\n    MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(42, CodecProfileLevel.AACObjectXHE);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.mediacodec;\n\nimport android.media.MediaFormat;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport java.nio.ByteBuffer;\nimport java.util.List;\n\n/** Helper class for configuring {@link MediaFormat} instances. */\npublic final class MediaFormatUtil {\n\n  private MediaFormatUtil() {}\n\n  /**\n   * Sets a {@link MediaFormat} {@link String} value.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param key The key to set.\n   * @param value The value to set.\n   */\n  public static void setString(MediaFormat format, String key, String value) {\n    format.setString(key, value);\n  }\n\n  /**\n   * Sets a {@link MediaFormat}'s codec specific data buffers.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param csdBuffers The csd buffers to set.\n   */\n  public static void setCsdBuffers(MediaFormat format, List<byte[]> csdBuffers) {\n    for (int i = 0; i < csdBuffers.size(); i++) {\n      format.setByteBuffer(\"csd-\" + i, ByteBuffer.wrap(csdBuffers.get(i)));\n    }\n  }\n\n  /**\n   * Sets a {@link MediaFormat} integer value. Does nothing if {@code value} is {@link\n   * Format#NO_VALUE}.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param key The key to set.\n   * @param value The value to set.\n   */\n  public static void maybeSetInteger(MediaFormat format, String key, int value) {\n    if (value != Format.NO_VALUE) {\n      format.setInteger(key, value);\n    }\n  }\n\n  /**\n   * Sets a {@link MediaFormat} float value. Does nothing if {@code value} is {@link\n   * Format#NO_VALUE}.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param key The key to set.\n   * @param value The value to set.\n   */\n  public static void maybeSetFloat(MediaFormat format, String key, float value) {\n    if (value != Format.NO_VALUE) {\n      format.setFloat(key, value);\n    }\n  }\n\n  /**\n   * Sets a {@link MediaFormat} {@link ByteBuffer} value. Does nothing if {@code value} is null.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param key The key to set.\n   * @param value The {@link byte[]} that will be wrapped to obtain the value.\n   */\n  public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) {\n    if (value != null) {\n      format.setByteBuffer(key, ByteBuffer.wrap(value));\n    }\n  }\n\n  /**\n   * Sets a {@link MediaFormat}'s color information. Does nothing if {@code colorInfo} is null.\n   *\n   * @param format The {@link MediaFormat} being configured.\n   * @param colorInfo The color info to set.\n   */\n  @SuppressWarnings(\"InlinedApi\")\n  public static void maybeSetColorInfo(MediaFormat format, @Nullable ColorInfo colorInfo) {\n    if (colorInfo != null) {\n      maybeSetInteger(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);\n      maybeSetInteger(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace);\n      maybeSetInteger(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);\n      maybeSetByteBuffer(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.mediacodec;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * A collection of metadata entries.\n */\npublic final class Metadata implements Parcelable {\n\n  /** A metadata entry. */\n  public interface Entry extends Parcelable {\n\n    /**\n     * Returns the {@link Format} that can be used to decode the wrapped metadata in {@link\n     * #getWrappedMetadataBytes()}, or null if this Entry doesn't contain wrapped metadata.\n     */\n    @Nullable\n    default Format getWrappedMetadataFormat() {\n      return null;\n    }\n\n    /**\n     * Returns the bytes of the wrapped metadata in this Entry, or null if it doesn't contain\n     * wrapped metadata.\n     */\n    @Nullable\n    default byte[] getWrappedMetadataBytes() {\n      return null;\n    }\n  }\n\n  private final Entry[] entries;\n\n  /**\n   * @param entries The metadata entries.\n   */\n  public Metadata(Entry... entries) {\n    this.entries = entries;\n  }\n\n  /**\n   * @param entries The metadata entries.\n   */\n  public Metadata(List<? extends Entry> entries) {\n    this.entries = new Entry[entries.size()];\n    entries.toArray(this.entries);\n  }\n\n  /* package */ Metadata(Parcel in) {\n    entries = new Entry[in.readInt()];\n    for (int i = 0; i < entries.length; i++) {\n      entries[i] = in.readParcelable(Entry.class.getClassLoader());\n    }\n  }\n\n  /**\n   * Returns the number of metadata entries.\n   */\n  public int length() {\n    return entries.length;\n  }\n\n  /**\n   * Returns the entry at the specified index.\n   *\n   * @param index The index of the entry.\n   * @return The entry at the specified index.\n   */\n  public Entry get(int index) {\n    return entries[index];\n  }\n\n  /**\n   * Returns a copy of this metadata with the entries of the specified metadata appended. Returns\n   * this instance if {@code other} is null.\n   *\n   * @param other The metadata that holds the entries to append. If null, this methods returns this\n   *     instance.\n   * @return The metadata instance with the appended entries.\n   */\n  public Metadata copyWithAppendedEntriesFrom(@Nullable Metadata other) {\n    if (other == null) {\n      return this;\n    }\n    return copyWithAppendedEntries(other.entries);\n  }\n\n  /**\n   * Returns a copy of this metadata with the specified entries appended.\n   *\n   * @param entriesToAppend The entries to append.\n   * @return The metadata instance with the appended entries.\n   */\n  public Metadata copyWithAppendedEntries(Entry... entriesToAppend) {\n    if (entriesToAppend.length == 0) {\n      return this;\n    }\n    return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend));\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    Metadata other = (Metadata) obj;\n    return Arrays.equals(entries, other.entries);\n  }\n\n  @Override\n  public int hashCode() {\n    return Arrays.hashCode(entries);\n  }\n\n  @Override\n  public String toString() {\n    return \"entries=\" + Arrays.toString(entries);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(entries.length);\n    for (Entry entry : entries) {\n      dest.writeParcelable(entry, 0);\n    }\n  }\n\n  public static final Creator<Metadata> CREATOR =\n      new Creator<Metadata>() {\n        @Override\n        public Metadata createFromParcel(Parcel in) {\n          return new Metadata(in);\n        }\n\n        @Override\n        public Metadata[] newArray(int size) {\n          return new Metadata[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Decodes metadata from binary data.\n */\npublic interface MetadataDecoder {\n\n  /**\n   * Decodes a {@link Metadata} element from the provided input buffer.\n   *\n   * @param inputBuffer The input buffer to decode.\n   * @return The decoded metadata object, or null if the metadata could not be decoded.\n   */\n  @Nullable\n  Metadata decode(MetadataInputBuffer inputBuffer);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;\nimport com.google.android.exoplayer2.metadata.icy.IcyDecoder;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder;\nimport com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * A factory for {@link MetadataDecoder} instances.\n */\npublic interface MetadataDecoderFactory {\n\n  /**\n   * Returns whether the factory is able to instantiate a {@link MetadataDecoder} for the given\n   * {@link Format}.\n   *\n   * @param format The {@link Format}.\n   * @return Whether the factory can instantiate a suitable {@link MetadataDecoder}.\n   */\n  boolean supportsFormat(Format format);\n\n  /**\n   * Creates a {@link MetadataDecoder} for the given {@link Format}.\n   *\n   * @param format The {@link Format}.\n   * @return A new {@link MetadataDecoder}.\n   * @throws IllegalArgumentException If the {@link Format} is not supported.\n   */\n  MetadataDecoder createDecoder(Format format);\n\n  /**\n   * Default {@link MetadataDecoder} implementation.\n   *\n   * <p>The formats supported by this factory are:\n   *\n   * <ul>\n   *   <li>ID3 ({@link Id3Decoder})\n   *   <li>EMSG ({@link EventMessageDecoder})\n   *   <li>SCTE-35 ({@link SpliceInfoDecoder})\n   *   <li>ICY ({@link IcyDecoder})\n   * </ul>\n   */\n  MetadataDecoderFactory DEFAULT =\n      new MetadataDecoderFactory() {\n\n        @Override\n        public boolean supportsFormat(Format format) {\n          @Nullable String mimeType = format.sampleMimeType;\n          return MimeTypes.APPLICATION_ID3.equals(mimeType)\n              || MimeTypes.APPLICATION_EMSG.equals(mimeType)\n              || MimeTypes.APPLICATION_SCTE35.equals(mimeType)\n              || MimeTypes.APPLICATION_ICY.equals(mimeType);\n        }\n\n        @Override\n        public MetadataDecoder createDecoder(Format format) {\n          @Nullable String mimeType = format.sampleMimeType;\n          if (mimeType != null) {\n            switch (mimeType) {\n              case MimeTypes.APPLICATION_ID3:\n                return new Id3Decoder();\n              case MimeTypes.APPLICATION_EMSG:\n                return new EventMessageDecoder();\n              case MimeTypes.APPLICATION_SCTE35:\n                return new SpliceInfoDecoder();\n              case MimeTypes.APPLICATION_ICY:\n                return new IcyDecoder();\n              default:\n                break;\n            }\n          }\n          throw new IllegalArgumentException(\n              \"Attempted to create decoder for unsupported MIME type: \" + mimeType);\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\n\n/**\n * A {@link DecoderInputBuffer} for a {@link MetadataDecoder}.\n */\npublic final class MetadataInputBuffer extends DecoderInputBuffer {\n\n  /**\n   * An offset that must be added to the metadata's timestamps after it's been decoded, or\n   * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.\n   */\n  public long subsampleOffsetUs;\n\n  public MetadataInputBuffer() {\n    super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\n/**\n * Receives metadata output.\n */\npublic interface MetadataOutput {\n\n  /**\n   * Called when there is metadata associated with current playback time.\n   *\n   * @param metadata The metadata.\n   */\n  void onMetadata(Metadata metadata);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Handler;\nimport android.os.Handler.Callback;\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A renderer for metadata.\n */\npublic final class MetadataRenderer extends BaseRenderer implements Callback {\n\n  private static final int MSG_INVOKE_RENDERER = 0;\n  // TODO: Holding multiple pending metadata objects is temporary mitigation against\n  // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been\n  // addressed.\n  private static final int MAX_PENDING_METADATA_COUNT = 5;\n\n  private final MetadataDecoderFactory decoderFactory;\n  private final MetadataOutput output;\n  @Nullable private final Handler outputHandler;\n  private final MetadataInputBuffer buffer;\n  private final @NullableType Metadata[] pendingMetadata;\n  private final long[] pendingMetadataTimestamps;\n\n  private int pendingMetadataIndex;\n  private int pendingMetadataCount;\n  @Nullable private MetadataDecoder decoder;\n  private boolean inputStreamEnded;\n  private long subsampleOffsetUs;\n\n  /**\n   * @param output The output.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   *     If the output makes use of standard Android UI components, then this should normally be the\n   *     looper associated with the application's main thread, which can be obtained using {@link\n   *     android.app.Activity#getMainLooper()}. Null may be passed if the output should be called\n   *     directly on the player's internal rendering thread.\n   */\n  public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {\n    this(output, outputLooper, MetadataDecoderFactory.DEFAULT);\n  }\n\n  /**\n   * @param output The output.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   *     If the output makes use of standard Android UI components, then this should normally be the\n   *     looper associated with the application's main thread, which can be obtained using {@link\n   *     android.app.Activity#getMainLooper()}. Null may be passed if the output should be called\n   *     directly on the player's internal rendering thread.\n   * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.\n   */\n  public MetadataRenderer(\n      MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) {\n    super(C.TRACK_TYPE_METADATA);\n    this.output = Assertions.checkNotNull(output);\n    this.outputHandler =\n        outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);\n    this.decoderFactory = Assertions.checkNotNull(decoderFactory);\n    buffer = new MetadataInputBuffer();\n    pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT];\n    pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT];\n  }\n\n  @Override\n  @Capabilities\n  public int supportsFormat(Format format) {\n    if (decoderFactory.supportsFormat(format)) {\n      return RendererCapabilities.create(\n          supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM);\n    } else {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) {\n    decoder = decoderFactory.createDecoder(formats[0]);\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) {\n    flushPendingMetadata();\n    inputStreamEnded = false;\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) {\n    if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) {\n      buffer.clear();\n      FormatHolder formatHolder = getFormatHolder();\n      int result = readSource(formatHolder, buffer, false);\n      if (result == C.RESULT_BUFFER_READ) {\n        if (buffer.isEndOfStream()) {\n          inputStreamEnded = true;\n        } else if (buffer.isDecodeOnly()) {\n          // Do nothing. Note this assumes that all metadata buffers can be decoded independently.\n          // If we ever need to support a metadata format where this is not the case, we'll need to\n          // pass the buffer to the decoder and discard the output.\n        } else {\n          buffer.subsampleOffsetUs = subsampleOffsetUs;\n          buffer.flip();\n          @Nullable Metadata metadata = castNonNull(decoder).decode(buffer);\n          if (metadata != null) {\n            List<Metadata.Entry> entries = new ArrayList<>(metadata.length());\n            decodeWrappedMetadata(metadata, entries);\n            if (!entries.isEmpty()) {\n              Metadata expandedMetadata = new Metadata(entries);\n              int index =\n                  (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;\n              pendingMetadata[index] = expandedMetadata;\n              pendingMetadataTimestamps[index] = buffer.timeUs;\n              pendingMetadataCount++;\n            }\n          }\n        }\n      } else if (result == C.RESULT_FORMAT_READ) {\n        subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs;\n      }\n    }\n\n    if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) {\n      Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]);\n      invokeRenderer(metadata);\n      pendingMetadata[pendingMetadataIndex] = null;\n      pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT;\n      pendingMetadataCount--;\n    }\n  }\n\n  /**\n   * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped\n   * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion\n   * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter).\n   */\n  private void decodeWrappedMetadata(Metadata metadata, List<Metadata.Entry> decodedEntries) {\n    for (int i = 0; i < metadata.length(); i++) {\n      @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat();\n      if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) {\n        MetadataDecoder wrappedMetadataDecoder =\n            decoderFactory.createDecoder(wrappedMetadataFormat);\n        // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too.\n        byte[] wrappedMetadataBytes =\n            Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes());\n        buffer.clear();\n        buffer.ensureSpaceForWrite(wrappedMetadataBytes.length);\n        castNonNull(buffer.data).put(wrappedMetadataBytes);\n        buffer.flip();\n        @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer);\n        if (innerMetadata != null) {\n          // The decoding succeeded, so we'll try another level of unwrapping.\n          decodeWrappedMetadata(innerMetadata, decodedEntries);\n        }\n      } else {\n        // Entry doesn't contain any wrapped metadata, so output it directly.\n        decodedEntries.add(metadata.get(i));\n      }\n    }\n  }\n\n  @Override\n  protected void onDisabled() {\n    flushPendingMetadata();\n    decoder = null;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return inputStreamEnded;\n  }\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  private void invokeRenderer(Metadata metadata) {\n    if (outputHandler != null) {\n      outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();\n    } else {\n      invokeRendererInternal(metadata);\n    }\n  }\n\n  private void flushPendingMetadata() {\n    Arrays.fill(pendingMetadata, null);\n    pendingMetadataIndex = 0;\n    pendingMetadataCount = 0;\n  }\n\n  @Override\n  public boolean handleMessage(Message msg) {\n    switch (msg.what) {\n      case MSG_INVOKE_RENDERER:\n        invokeRendererInternal((Metadata) msg.obj);\n        return true;\n      default:\n        // Should never happen.\n        throw new IllegalStateException();\n    }\n  }\n\n  private void invokeRendererInternal(Metadata metadata) {\n    output.onMetadata(metadata);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.emsg;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/** An Event Message (emsg) as defined in ISO 23009-1. */\npublic final class EventMessage implements Metadata.Entry {\n\n  /**\n   * emsg scheme_id_uri from the <a href=\"https://aomediacodec.github.io/av1-id3/#semantics\">CMAF\n   * spec</a>.\n   */\n  @VisibleForTesting public static final String ID3_SCHEME_ID_AOM = \"https://aomedia.org/emsg/ID3\";\n\n  /**\n   * The Apple-hosted scheme_id equivalent to {@code ID3_SCHEME_ID_AOM} - used before AOM adoption.\n   */\n  private static final String ID3_SCHEME_ID_APPLE =\n      \"https://developer.apple.com/streaming/emsg-id3\";\n\n  /**\n   * scheme_id_uri from section 7.3.2 of <a\n   * href=\"https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%20214-3%202015.pdf\">SCTE 214-3\n   * 2015</a>.\n   */\n  @VisibleForTesting public static final String SCTE35_SCHEME_ID = \"urn:scte:scte35:2014:bin\";\n\n  private static final Format ID3_FORMAT =\n      Format.createSampleFormat(\n          /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE);\n  private static final Format SCTE35_FORMAT =\n      Format.createSampleFormat(\n          /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE);\n\n  /** The message scheme. */\n  public final String schemeIdUri;\n\n  /**\n   * The value for the event.\n   */\n  public final String value;\n\n  /**\n   * The duration of the event in milliseconds.\n   */\n  public final long durationMs;\n\n  /**\n   * The instance identifier.\n   */\n  public final long id;\n\n  /**\n   * The body of the message.\n   */\n  public final byte[] messageData;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /**\n   * @param schemeIdUri The message scheme.\n   * @param value The value for the event.\n   * @param durationMs The duration of the event in milliseconds.\n   * @param id The instance identifier.\n   * @param messageData The body of the message.\n   */\n  public EventMessage(\n      String schemeIdUri, String value, long durationMs, long id, byte[] messageData) {\n    this.schemeIdUri = schemeIdUri;\n    this.value = value;\n    this.durationMs = durationMs;\n    this.id = id;\n    this.messageData = messageData;\n  }\n\n  /* package */ EventMessage(Parcel in) {\n    schemeIdUri = castNonNull(in.readString());\n    value = castNonNull(in.readString());\n    durationMs = in.readLong();\n    id = in.readLong();\n    messageData = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  @Nullable\n  public Format getWrappedMetadataFormat() {\n    switch (schemeIdUri) {\n      case ID3_SCHEME_ID_AOM:\n      case ID3_SCHEME_ID_APPLE:\n        return ID3_FORMAT;\n      case SCTE35_SCHEME_ID:\n        return SCTE35_FORMAT;\n      default:\n        return null;\n    }\n  }\n\n  @Override\n  @Nullable\n  public byte[] getWrappedMetadataBytes() {\n    return getWrappedMetadataFormat() != null ? messageData : null;\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 17;\n      result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0);\n      result = 31 * result + (value != null ? value.hashCode() : 0);\n      result = 31 * result + (int) (durationMs ^ (durationMs >>> 32));\n      result = 31 * result + (int) (id ^ (id >>> 32));\n      result = 31 * result + Arrays.hashCode(messageData);\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    EventMessage other = (EventMessage) obj;\n    return durationMs == other.durationMs\n        && id == other.id\n        && Util.areEqual(schemeIdUri, other.schemeIdUri)\n        && Util.areEqual(value, other.value)\n        && Arrays.equals(messageData, other.messageData);\n  }\n\n  @Override\n  public String toString() {\n    return \"EMSG: scheme=\"\n        + schemeIdUri\n        + \", id=\"\n        + id\n        + \", durationMs=\"\n        + durationMs\n        + \", value=\"\n        + value;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(schemeIdUri);\n    dest.writeString(value);\n    dest.writeLong(durationMs);\n    dest.writeLong(id);\n    dest.writeByteArray(messageData);\n  }\n\n  public static final Parcelable.Creator<EventMessage> CREATOR =\n      new Parcelable.Creator<EventMessage>() {\n\n    @Override\n    public EventMessage createFromParcel(Parcel in) {\n      return new EventMessage(in);\n    }\n\n    @Override\n    public EventMessage[] newArray(int size) {\n      return new EventMessage[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.emsg;\n\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataDecoder;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\n\n/** Decodes data encoded by {@link EventMessageEncoder}. */\npublic final class EventMessageDecoder implements MetadataDecoder {\n\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  @Override\n  public Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);\n    byte[] data = buffer.array();\n    int size = buffer.limit();\n    return new Metadata(decode(new ParsableByteArray(data, size)));\n  }\n\n  public EventMessage decode(ParsableByteArray emsgData) {\n    String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString());\n    String value = Assertions.checkNotNull(emsgData.readNullTerminatedString());\n    long durationMs = emsgData.readUnsignedInt();\n    long id = emsgData.readUnsignedInt();\n    byte[] messageData =\n        Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit());\n    return new EventMessage(schemeIdUri, value, durationMs, id, messageData);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.emsg;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.DataOutputStream;\nimport java.io.IOException;\n\n/**\n * Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe.\n */\npublic final class EventMessageEncoder {\n\n  private final ByteArrayOutputStream byteArrayOutputStream;\n  private final DataOutputStream dataOutputStream;\n\n  public EventMessageEncoder() {\n    byteArrayOutputStream = new ByteArrayOutputStream(512);\n    dataOutputStream = new DataOutputStream(byteArrayOutputStream);\n  }\n\n  /**\n   * Encodes an {@link EventMessage} to a byte array that can be decoded by {@link\n   * EventMessageDecoder}.\n   *\n   * @param eventMessage The event message to be encoded.\n   * @return The serialized byte array.\n   */\n  public byte[] encode(EventMessage eventMessage) {\n    byteArrayOutputStream.reset();\n    try {\n      writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri);\n      String nonNullValue = eventMessage.value != null ? eventMessage.value : \"\";\n      writeNullTerminatedString(dataOutputStream, nonNullValue);\n      writeUnsignedInt(dataOutputStream, eventMessage.durationMs);\n      writeUnsignedInt(dataOutputStream, eventMessage.id);\n      dataOutputStream.write(eventMessage.messageData);\n      dataOutputStream.flush();\n      return byteArrayOutputStream.toByteArray();\n    } catch (IOException e) {\n      // Should never happen.\n      throw new RuntimeException(e);\n    }\n  }\n\n  private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value)\n      throws IOException {\n    dataOutputStream.writeBytes(value);\n    dataOutputStream.writeByte(0);\n  }\n\n  private static void writeUnsignedInt(DataOutputStream outputStream, long value)\n      throws IOException {\n    outputStream.writeByte((int) (value >>> 24) & 0xFF);\n    outputStream.writeByte((int) (value >>> 16) & 0xFF);\n    outputStream.writeByte((int) (value >>> 8) & 0xFF);\n    outputStream.writeByte((int) value & 0xFF);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata.emsg;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.flac;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport java.util.Arrays;\n\n/** A picture parsed from a FLAC file. */\npublic final class PictureFrame implements Metadata.Entry {\n\n  /** The type of the picture. */\n  public final int pictureType;\n  /** The mime type of the picture. */\n  public final String mimeType;\n  /** A description of the picture. */\n  public final String description;\n  /** The width of the picture in pixels. */\n  public final int width;\n  /** The height of the picture in pixels. */\n  public final int height;\n  /** The color depth of the picture in bits-per-pixel. */\n  public final int depth;\n  /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */\n  public final int colors;\n  /** The encoded picture data. */\n  public final byte[] pictureData;\n\n  public PictureFrame(\n      int pictureType,\n      String mimeType,\n      String description,\n      int width,\n      int height,\n      int depth,\n      int colors,\n      byte[] pictureData) {\n    this.pictureType = pictureType;\n    this.mimeType = mimeType;\n    this.description = description;\n    this.width = width;\n    this.height = height;\n    this.depth = depth;\n    this.colors = colors;\n    this.pictureData = pictureData;\n  }\n\n  /* package */ PictureFrame(Parcel in) {\n    this.pictureType = in.readInt();\n    this.mimeType = castNonNull(in.readString());\n    this.description = castNonNull(in.readString());\n    this.width = in.readInt();\n    this.height = in.readInt();\n    this.depth = in.readInt();\n    this.colors = in.readInt();\n    this.pictureData = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  public String toString() {\n    return \"Picture: mimeType=\" + mimeType + \", description=\" + description;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    PictureFrame other = (PictureFrame) obj;\n    return (pictureType == other.pictureType)\n        && mimeType.equals(other.mimeType)\n        && description.equals(other.description)\n        && (width == other.width)\n        && (height == other.height)\n        && (depth == other.depth)\n        && (colors == other.colors)\n        && Arrays.equals(pictureData, other.pictureData);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + pictureType;\n    result = 31 * result + mimeType.hashCode();\n    result = 31 * result + description.hashCode();\n    result = 31 * result + width;\n    result = 31 * result + height;\n    result = 31 * result + depth;\n    result = 31 * result + colors;\n    result = 31 * result + Arrays.hashCode(pictureData);\n    return result;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(pictureType);\n    dest.writeString(mimeType);\n    dest.writeString(description);\n    dest.writeInt(width);\n    dest.writeInt(height);\n    dest.writeInt(depth);\n    dest.writeInt(colors);\n    dest.writeByteArray(pictureData);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<PictureFrame> CREATOR =\n      new Parcelable.Creator<PictureFrame>() {\n\n        @Override\n        public PictureFrame createFromParcel(Parcel in) {\n          return new PictureFrame(in);\n        }\n\n        @Override\n        public PictureFrame[] newArray(int size) {\n          return new PictureFrame[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.flac;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\n\n/** A vorbis comment. */\npublic final class VorbisComment implements Metadata.Entry {\n\n  /** The key. */\n  public final String key;\n\n  /** The value. */\n  public final String value;\n\n  /**\n   * @param key The key.\n   * @param value The value.\n   */\n  public VorbisComment(String key, String value) {\n    this.key = key;\n    this.value = value;\n  }\n\n  /* package */ VorbisComment(Parcel in) {\n    this.key = castNonNull(in.readString());\n    this.value = castNonNull(in.readString());\n  }\n\n  @Override\n  public String toString() {\n    return \"VC: \" + key + \"=\" + value;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    VorbisComment other = (VorbisComment) obj;\n    return key.equals(other.key) && value.equals(other.value);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + key.hashCode();\n    result = 31 * result + value.hashCode();\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(key);\n    dest.writeString(value);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<VorbisComment> CREATOR =\n      new Parcelable.Creator<VorbisComment>() {\n\n        @Override\n        public VorbisComment createFromParcel(Parcel in) {\n          return new VorbisComment(in);\n        }\n\n        @Override\n        public VorbisComment[] newArray(int size) {\n          return new VorbisComment[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata.flac;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.icy;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataDecoder;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/** Decodes ICY stream information. */\npublic final class IcyDecoder implements MetadataDecoder {\n\n  private static final Pattern METADATA_ELEMENT = Pattern.compile(\"(.+?)='(.*?)';\", Pattern.DOTALL);\n  private static final String STREAM_KEY_NAME = \"streamtitle\";\n  private static final String STREAM_KEY_URL = \"streamurl\";\n\n  @Override\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  public Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);\n    byte[] data = buffer.array();\n    int length = buffer.limit();\n    return decode(Util.fromUtf8Bytes(data, 0, length));\n  }\n\n  @VisibleForTesting\n  /* package */ Metadata decode(String metadata) {\n    @Nullable String name = null;\n    @Nullable String url = null;\n    int index = 0;\n    Matcher matcher = METADATA_ELEMENT.matcher(metadata);\n    while (matcher.find(index)) {\n      String key = Util.toLowerInvariant(matcher.group(1));\n      String value = matcher.group(2);\n      switch (key) {\n        case STREAM_KEY_NAME:\n          name = value;\n          break;\n        case STREAM_KEY_URL:\n          url = value;\n          break;\n      }\n      index = matcher.end();\n    }\n    return new Metadata(new IcyInfo(metadata, name, url));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.icy;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\nimport java.util.Map;\n\n/** ICY headers. */\npublic final class IcyHeaders implements Metadata.Entry {\n\n  public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = \"Icy-MetaData\";\n  public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = \"1\";\n\n  private static final String TAG = \"IcyHeaders\";\n\n  private static final String RESPONSE_HEADER_BITRATE = \"icy-br\";\n  private static final String RESPONSE_HEADER_GENRE = \"icy-genre\";\n  private static final String RESPONSE_HEADER_NAME = \"icy-name\";\n  private static final String RESPONSE_HEADER_URL = \"icy-url\";\n  private static final String RESPONSE_HEADER_PUB = \"icy-pub\";\n  private static final String RESPONSE_HEADER_METADATA_INTERVAL = \"icy-metaint\";\n\n  /**\n   * Parses {@link IcyHeaders} from response headers.\n   *\n   * @param responseHeaders The response headers.\n   * @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.\n   */\n  @Nullable\n  public static IcyHeaders parse(Map<String, List<String>> responseHeaders) {\n    boolean icyHeadersPresent = false;\n    int bitrate = Format.NO_VALUE;\n    String genre = null;\n    String name = null;\n    String url = null;\n    boolean isPublic = false;\n    int metadataInterval = C.LENGTH_UNSET;\n\n    List<String> headers = responseHeaders.get(RESPONSE_HEADER_BITRATE);\n    if (headers != null) {\n      String bitrateHeader = headers.get(0);\n      try {\n        bitrate = Integer.parseInt(bitrateHeader) * 1000;\n        if (bitrate > 0) {\n          icyHeadersPresent = true;\n        } else {\n          Log.w(TAG, \"Invalid bitrate: \" + bitrateHeader);\n          bitrate = Format.NO_VALUE;\n        }\n      } catch (NumberFormatException e) {\n        Log.w(TAG, \"Invalid bitrate header: \" + bitrateHeader);\n      }\n    }\n    headers = responseHeaders.get(RESPONSE_HEADER_GENRE);\n    if (headers != null) {\n      genre = headers.get(0);\n      icyHeadersPresent = true;\n    }\n    headers = responseHeaders.get(RESPONSE_HEADER_NAME);\n    if (headers != null) {\n      name = headers.get(0);\n      icyHeadersPresent = true;\n    }\n    headers = responseHeaders.get(RESPONSE_HEADER_URL);\n    if (headers != null) {\n      url = headers.get(0);\n      icyHeadersPresent = true;\n    }\n    headers = responseHeaders.get(RESPONSE_HEADER_PUB);\n    if (headers != null) {\n      isPublic = headers.get(0).equals(\"1\");\n      icyHeadersPresent = true;\n    }\n    headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL);\n    if (headers != null) {\n      String metadataIntervalHeader = headers.get(0);\n      try {\n        metadataInterval = Integer.parseInt(metadataIntervalHeader);\n        if (metadataInterval > 0) {\n          icyHeadersPresent = true;\n        } else {\n          Log.w(TAG, \"Invalid metadata interval: \" + metadataIntervalHeader);\n          metadataInterval = C.LENGTH_UNSET;\n        }\n      } catch (NumberFormatException e) {\n        Log.w(TAG, \"Invalid metadata interval: \" + metadataIntervalHeader);\n      }\n    }\n    return icyHeadersPresent\n        ? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval)\n        : null;\n  }\n\n  /**\n   * Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header\n   * was not present.\n   */\n  public final int bitrate;\n  /** The genre ({@code icy-genre}). */\n  @Nullable public final String genre;\n  /** The stream name ({@code icy-name}). */\n  @Nullable public final String name;\n  /** The URL of the radio station ({@code icy-url}). */\n  @Nullable public final String url;\n  /**\n   * Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not\n   * present.\n   */\n  public final boolean isPublic;\n\n  /**\n   * The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET}\n   * if the header was not present.\n   */\n  public final int metadataInterval;\n\n  /**\n   * @param bitrate See {@link #bitrate}.\n   * @param genre See {@link #genre}.\n   * @param name See {@link #name See}.\n   * @param url See {@link #url}.\n   * @param isPublic See {@link #isPublic}.\n   * @param metadataInterval See {@link #metadataInterval}.\n   */\n  public IcyHeaders(\n      int bitrate,\n      @Nullable String genre,\n      @Nullable String name,\n      @Nullable String url,\n      boolean isPublic,\n      int metadataInterval) {\n    Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0);\n    this.bitrate = bitrate;\n    this.genre = genre;\n    this.name = name;\n    this.url = url;\n    this.isPublic = isPublic;\n    this.metadataInterval = metadataInterval;\n  }\n\n  /* package */ IcyHeaders(Parcel in) {\n    bitrate = in.readInt();\n    genre = in.readString();\n    name = in.readString();\n    url = in.readString();\n    isPublic = Util.readBoolean(in);\n    metadataInterval = in.readInt();\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    IcyHeaders other = (IcyHeaders) obj;\n    return bitrate == other.bitrate\n        && Util.areEqual(genre, other.genre)\n        && Util.areEqual(name, other.name)\n        && Util.areEqual(url, other.url)\n        && isPublic == other.isPublic\n        && metadataInterval == other.metadataInterval;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + bitrate;\n    result = 31 * result + (genre != null ? genre.hashCode() : 0);\n    result = 31 * result + (name != null ? name.hashCode() : 0);\n    result = 31 * result + (url != null ? url.hashCode() : 0);\n    result = 31 * result + (isPublic ? 1 : 0);\n    result = 31 * result + metadataInterval;\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return \"IcyHeaders: name=\\\"\"\n        + name\n        + \"\\\", genre=\\\"\"\n        + genre\n        + \"\\\", bitrate=\"\n        + bitrate\n        + \", metadataInterval=\"\n        + metadataInterval;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(bitrate);\n    dest.writeString(genre);\n    dest.writeString(name);\n    dest.writeString(url);\n    Util.writeBoolean(dest, isPublic);\n    dest.writeInt(metadataInterval);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<IcyHeaders> CREATOR =\n      new Parcelable.Creator<IcyHeaders>() {\n\n        @Override\n        public IcyHeaders createFromParcel(Parcel in) {\n          return new IcyHeaders(in);\n        }\n\n        @Override\n        public IcyHeaders[] newArray(int size) {\n          return new IcyHeaders[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.icy;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/** ICY in-stream information. */\npublic final class IcyInfo implements Metadata.Entry {\n\n  /** The complete metadata string used to construct this IcyInfo. */\n  public final String rawMetadata;\n  /** The stream title if present, or {@code null}. */\n  @Nullable public final String title;\n  /** The stream URL if present, or {@code null}. */\n  @Nullable public final String url;\n\n  /**\n   * Construct a new IcyInfo from the source metadata string, and optionally a StreamTitle and\n   * StreamUrl that have been extracted.\n   *\n   * @param rawMetadata See {@link #rawMetadata}.\n   * @param title See {@link #title}.\n   * @param url See {@link #url}.\n   */\n  public IcyInfo(String rawMetadata, @Nullable String title, @Nullable String url) {\n    this.rawMetadata = rawMetadata;\n    this.title = title;\n    this.url = url;\n  }\n\n  /* package */ IcyInfo(Parcel in) {\n    rawMetadata = Assertions.checkNotNull(in.readString());\n    title = in.readString();\n    url = in.readString();\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    IcyInfo other = (IcyInfo) obj;\n    // title & url are derived from rawMetadata, so no need to include them in the comparison.\n    return Util.areEqual(rawMetadata, other.rawMetadata);\n  }\n\n  @Override\n  public int hashCode() {\n    // title & url are derived from rawMetadata, so no need to include them in the hash.\n    return rawMetadata.hashCode();\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\n        \"ICY: title=\\\"%s\\\", url=\\\"%s\\\", rawMetadata=\\\"%s\\\"\", title, url, rawMetadata);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(rawMetadata);\n    dest.writeString(title);\n    dest.writeString(url);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<IcyInfo> CREATOR =\n      new Parcelable.Creator<IcyInfo>() {\n\n        @Override\n        public IcyInfo createFromParcel(Parcel in) {\n          return new IcyInfo(in);\n        }\n\n        @Override\n        public IcyInfo[] newArray(int size) {\n          return new IcyInfo[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata.icy;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * APIC (Attached Picture) ID3 frame.\n */\npublic final class ApicFrame extends Id3Frame {\n\n  public static final String ID = \"APIC\";\n\n  public final String mimeType;\n  @Nullable public final String description;\n  public final int pictureType;\n  public final byte[] pictureData;\n\n  public ApicFrame(\n      String mimeType, @Nullable String description, int pictureType, byte[] pictureData) {\n    super(ID);\n    this.mimeType = mimeType;\n    this.description = description;\n    this.pictureType = pictureType;\n    this.pictureData = pictureData;\n  }\n\n  /* package */ ApicFrame(Parcel in) {\n    super(ID);\n    mimeType = castNonNull(in.readString());\n    description = in.readString();\n    pictureType = in.readInt();\n    pictureData = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    ApicFrame other = (ApicFrame) obj;\n    return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType)\n        && Util.areEqual(description, other.description)\n        && Arrays.equals(pictureData, other.pictureData);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + pictureType;\n    result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + Arrays.hashCode(pictureData);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": mimeType=\" + mimeType + \", description=\" + description;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(mimeType);\n    dest.writeString(description);\n    dest.writeInt(pictureType);\n    dest.writeByteArray(pictureData);\n  }\n\n  public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() {\n\n    @Override\n    public ApicFrame createFromParcel(Parcel in) {\n      return new ApicFrame(in);\n    }\n\n    @Override\n    public ApicFrame[] newArray(int size) {\n      return new ApicFrame[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport java.util.Arrays;\n\n/**\n * Binary ID3 frame.\n */\npublic final class BinaryFrame extends Id3Frame {\n\n  public final byte[] data;\n\n  public BinaryFrame(String id, byte[] data) {\n    super(id);\n    this.data = data;\n  }\n\n  /* package */ BinaryFrame(Parcel in) {\n    super(castNonNull(in.readString()));\n    data = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    BinaryFrame other = (BinaryFrame) obj;\n    return id.equals(other.id) && Arrays.equals(data, other.data);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + id.hashCode();\n    result = 31 * result + Arrays.hashCode(data);\n    return result;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeByteArray(data);\n  }\n\n  public static final Parcelable.Creator<BinaryFrame> CREATOR =\n      new Parcelable.Creator<BinaryFrame>() {\n\n        @Override\n        public BinaryFrame createFromParcel(Parcel in) {\n          return new BinaryFrame(in);\n        }\n\n        @Override\n        public BinaryFrame[] newArray(int size) {\n          return new BinaryFrame[size];\n        }\n\n      };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Chapter information ID3 frame.\n */\npublic final class ChapterFrame extends Id3Frame {\n\n  public static final String ID = \"CHAP\";\n\n  public final String chapterId;\n  public final int startTimeMs;\n  public final int endTimeMs;\n  /**\n   * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set.\n   */\n  public final long startOffset;\n  /**\n   * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set.\n   */\n  public final long endOffset;\n  private final Id3Frame[] subFrames;\n\n  public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset,\n      long endOffset, Id3Frame[] subFrames) {\n    super(ID);\n    this.chapterId = chapterId;\n    this.startTimeMs = startTimeMs;\n    this.endTimeMs = endTimeMs;\n    this.startOffset = startOffset;\n    this.endOffset = endOffset;\n    this.subFrames = subFrames;\n  }\n\n  /* package */ ChapterFrame(Parcel in) {\n    super(ID);\n    this.chapterId = castNonNull(in.readString());\n    this.startTimeMs = in.readInt();\n    this.endTimeMs = in.readInt();\n    this.startOffset = in.readLong();\n    this.endOffset = in.readLong();\n    int subFrameCount = in.readInt();\n    subFrames = new Id3Frame[subFrameCount];\n    for (int i = 0; i < subFrameCount; i++) {\n      subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());\n    }\n  }\n\n  /**\n   * Returns the number of sub-frames.\n   */\n  public int getSubFrameCount() {\n    return subFrames.length;\n  }\n\n  /**\n   * Returns the sub-frame at {@code index}.\n   */\n  public Id3Frame getSubFrame(int index) {\n    return subFrames[index];\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    ChapterFrame other = (ChapterFrame) obj;\n    return startTimeMs == other.startTimeMs\n        && endTimeMs == other.endTimeMs\n        && startOffset == other.startOffset\n        && endOffset == other.endOffset\n        && Util.areEqual(chapterId, other.chapterId)\n        && Arrays.equals(subFrames, other.subFrames);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + startTimeMs;\n    result = 31 * result + endTimeMs;\n    result = 31 * result + (int) startOffset;\n    result = 31 * result + (int) endOffset;\n    result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(chapterId);\n    dest.writeInt(startTimeMs);\n    dest.writeInt(endTimeMs);\n    dest.writeLong(startOffset);\n    dest.writeLong(endOffset);\n    dest.writeInt(subFrames.length);\n    for (Id3Frame subFrame : subFrames) {\n      dest.writeParcelable(subFrame, 0);\n    }\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() {\n\n    @Override\n    public ChapterFrame createFromParcel(Parcel in) {\n      return new ChapterFrame(in);\n    }\n\n    @Override\n    public ChapterFrame[] newArray(int size) {\n      return new ChapterFrame[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Chapter table of contents ID3 frame.\n */\npublic final class ChapterTocFrame extends Id3Frame {\n\n  public static final String ID = \"CTOC\";\n\n  public final String elementId;\n  public final boolean isRoot;\n  public final boolean isOrdered;\n  public final String[] children;\n  private final Id3Frame[] subFrames;\n\n  public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children,\n      Id3Frame[] subFrames) {\n    super(ID);\n    this.elementId = elementId;\n    this.isRoot = isRoot;\n    this.isOrdered = isOrdered;\n    this.children = children;\n    this.subFrames = subFrames;\n  }\n\n  /* package */\n  ChapterTocFrame(Parcel in) {\n    super(ID);\n    this.elementId = castNonNull(in.readString());\n    this.isRoot = in.readByte() != 0;\n    this.isOrdered = in.readByte() != 0;\n    this.children = castNonNull(in.createStringArray());\n    int subFrameCount = in.readInt();\n    subFrames = new Id3Frame[subFrameCount];\n    for (int i = 0; i < subFrameCount; i++) {\n      subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());\n    }\n  }\n\n  /**\n   * Returns the number of sub-frames.\n   */\n  public int getSubFrameCount() {\n    return subFrames.length;\n  }\n\n  /**\n   * Returns the sub-frame at {@code index}.\n   */\n  public Id3Frame getSubFrame(int index) {\n    return subFrames[index];\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    ChapterTocFrame other = (ChapterTocFrame) obj;\n    return isRoot == other.isRoot\n        && isOrdered == other.isOrdered\n        && Util.areEqual(elementId, other.elementId)\n        && Arrays.equals(children, other.children)\n        && Arrays.equals(subFrames, other.subFrames);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (isRoot ? 1 : 0);\n    result = 31 * result + (isOrdered ? 1 : 0);\n    result = 31 * result + (elementId != null ? elementId.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(elementId);\n    dest.writeByte((byte) (isRoot ? 1 : 0));\n    dest.writeByte((byte) (isOrdered ? 1 : 0));\n    dest.writeStringArray(children);\n    dest.writeInt(subFrames.length);\n    for (Id3Frame subFrame : subFrames) {\n      dest.writeParcelable(subFrame, 0);\n    }\n  }\n\n  public static final Creator<ChapterTocFrame> CREATOR = new Creator<ChapterTocFrame>() {\n\n    @Override\n    public ChapterTocFrame createFromParcel(Parcel in) {\n      return new ChapterTocFrame(in);\n    }\n\n    @Override\n    public ChapterTocFrame[] newArray(int size) {\n      return new ChapterTocFrame[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Comment ID3 frame.\n */\npublic final class CommentFrame extends Id3Frame {\n\n  public static final String ID = \"COMM\";\n\n  public final String language;\n  public final String description;\n  public final String text;\n\n  public CommentFrame(String language, String description, String text) {\n    super(ID);\n    this.language = language;\n    this.description = description;\n    this.text = text;\n  }\n\n  /* package */ CommentFrame(Parcel in) {\n    super(ID);\n    language = castNonNull(in.readString());\n    description = castNonNull(in.readString());\n    text = castNonNull(in.readString());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    CommentFrame other = (CommentFrame) obj;\n    return Util.areEqual(description, other.description) && Util.areEqual(language, other.language)\n        && Util.areEqual(text, other.text);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (language != null ? language.hashCode() : 0);\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + (text != null ? text.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": language=\" + language + \", description=\" + description;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(language);\n    dest.writeString(text);\n  }\n\n  public static final Parcelable.Creator<CommentFrame> CREATOR =\n      new Parcelable.Creator<CommentFrame>() {\n\n        @Override\n        public CommentFrame createFromParcel(Parcel in) {\n          return new CommentFrame(in);\n        }\n\n        @Override\n        public CommentFrame[] newArray(int size) {\n          return new CommentFrame[size];\n        }\n\n      };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * GEOB (General Encapsulated Object) ID3 frame.\n */\npublic final class GeobFrame extends Id3Frame {\n\n  public static final String ID = \"GEOB\";\n\n  public final String mimeType;\n  public final String filename;\n  public final String description;\n  public final byte[] data;\n\n  public GeobFrame(String mimeType, String filename, String description, byte[] data) {\n    super(ID);\n    this.mimeType = mimeType;\n    this.filename = filename;\n    this.description = description;\n    this.data = data;\n  }\n\n  /* package */ GeobFrame(Parcel in) {\n    super(ID);\n    mimeType = castNonNull(in.readString());\n    filename = castNonNull(in.readString());\n    description = castNonNull(in.readString());\n    data = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    GeobFrame other = (GeobFrame) obj;\n    return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename)\n        && Util.areEqual(description, other.description) && Arrays.equals(data, other.data);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);\n    result = 31 * result + (filename != null ? filename.hashCode() : 0);\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + Arrays.hashCode(data);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id\n        + \": mimeType=\"\n        + mimeType\n        + \", filename=\"\n        + filename\n        + \", description=\"\n        + description;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(mimeType);\n    dest.writeString(filename);\n    dest.writeString(description);\n    dest.writeByteArray(data);\n  }\n\n  public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() {\n\n    @Override\n    public GeobFrame createFromParcel(Parcel in) {\n      return new GeobFrame(in);\n    }\n\n    @Override\n    public GeobFrame[] newArray(int size) {\n      return new GeobFrame[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataDecoder;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Locale;\n\n/**\n * Decodes ID3 tags.\n */\npublic final class Id3Decoder implements MetadataDecoder {\n\n  /**\n   * A predicate for determining whether individual frames should be decoded.\n   */\n  public interface FramePredicate {\n\n    /**\n     * Returns whether a frame with the specified parameters should be decoded.\n     *\n     * @param majorVersion The major version of the ID3 tag.\n     * @param id0 The first byte of the frame ID.\n     * @param id1 The second byte of the frame ID.\n     * @param id2 The third byte of the frame ID.\n     * @param id3 The fourth byte of the frame ID.\n     * @return Whether the frame should be decoded.\n     */\n    boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3);\n\n  }\n\n  /** A predicate that indicates no frames should be decoded. */\n  public static final FramePredicate NO_FRAMES_PREDICATE =\n      (majorVersion, id0, id1, id2, id3) -> false;\n\n  private static final String TAG = \"Id3Decoder\";\n\n  /** The first three bytes of a well formed ID3 tag header. */\n  public static final int ID3_TAG = 0x00494433;\n  /**\n   * Length of an ID3 tag header.\n   */\n  public static final int ID3_HEADER_LENGTH = 10;\n\n  private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080;\n  private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040;\n  private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020;\n  private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008;\n  private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004;\n  private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040;\n  private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002;\n  private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001;\n\n  private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;\n  private static final int ID3_TEXT_ENCODING_UTF_16 = 1;\n  private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;\n  private static final int ID3_TEXT_ENCODING_UTF_8 = 3;\n\n  @Nullable private final FramePredicate framePredicate;\n\n  public Id3Decoder() {\n    this(null);\n  }\n\n  /**\n   * @param framePredicate Determines which frames are decoded. May be null to decode all frames.\n   */\n  public Id3Decoder(@Nullable FramePredicate framePredicate) {\n    this.framePredicate = framePredicate;\n  }\n\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  @Override\n  @Nullable\n  public Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);\n    return decode(buffer.array(), buffer.limit());\n  }\n\n  /**\n   * Decodes ID3 tags.\n   *\n   * @param data The bytes to decode ID3 tags from.\n   * @param size Amount of bytes in {@code data} to read.\n   * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could\n   *     not be decoded.\n   */\n  @Nullable\n  public Metadata decode(byte[] data, int size) {\n    List<Id3Frame> id3Frames = new ArrayList<>();\n    ParsableByteArray id3Data = new ParsableByteArray(data, size);\n\n    Id3Header id3Header = decodeHeader(id3Data);\n    if (id3Header == null) {\n      return null;\n    }\n\n    int startPosition = id3Data.getPosition();\n    int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;\n    int framesSize = id3Header.framesSize;\n    if (id3Header.isUnsynchronized) {\n      framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);\n    }\n    id3Data.setLimit(startPosition + framesSize);\n\n    boolean unsignedIntFrameSizeHack = false;\n    if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) {\n      if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) {\n        unsignedIntFrameSizeHack = true;\n      } else {\n        Log.w(TAG, \"Failed to validate ID3 tag with majorVersion=\" + id3Header.majorVersion);\n        return null;\n      }\n    }\n\n    while (id3Data.bytesLeft() >= frameHeaderSize) {\n      Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,\n          frameHeaderSize, framePredicate);\n      if (frame != null) {\n        id3Frames.add(frame);\n      }\n    }\n\n    return new Metadata(id3Frames);\n  }\n\n  /**\n   * @param data A {@link ParsableByteArray} from which the header should be read.\n   * @return The parsed header, or null if the ID3 tag is unsupported.\n   */\n  @Nullable\n  private static Id3Header decodeHeader(ParsableByteArray data) {\n    if (data.bytesLeft() < ID3_HEADER_LENGTH) {\n      Log.w(TAG, \"Data too short to be an ID3 tag\");\n      return null;\n    }\n\n    int id = data.readUnsignedInt24();\n    if (id != ID3_TAG) {\n      Log.w(TAG, \"Unexpected first three bytes of ID3 tag header: 0x\" + String.format(\"%06X\", id));\n      return null;\n    }\n\n    int majorVersion = data.readUnsignedByte();\n    data.skipBytes(1); // Skip minor version.\n    int flags = data.readUnsignedByte();\n    int framesSize = data.readSynchSafeInt();\n\n    if (majorVersion == 2) {\n      boolean isCompressed = (flags & 0x40) != 0;\n      if (isCompressed) {\n        Log.w(TAG, \"Skipped ID3 tag with majorVersion=2 and undefined compression scheme\");\n        return null;\n      }\n    } else if (majorVersion == 3) {\n      boolean hasExtendedHeader = (flags & 0x40) != 0;\n      if (hasExtendedHeader) {\n        int extendedHeaderSize = data.readInt(); // Size excluding size field.\n        data.skipBytes(extendedHeaderSize);\n        framesSize -= (extendedHeaderSize + 4);\n      }\n    } else if (majorVersion == 4) {\n      boolean hasExtendedHeader = (flags & 0x40) != 0;\n      if (hasExtendedHeader) {\n        int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.\n        data.skipBytes(extendedHeaderSize - 4);\n        framesSize -= extendedHeaderSize;\n      }\n      boolean hasFooter = (flags & 0x10) != 0;\n      if (hasFooter) {\n        framesSize -= 10;\n      }\n    } else {\n      Log.w(TAG, \"Skipped ID3 tag with unsupported majorVersion=\" + majorVersion);\n      return null;\n    }\n\n    // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.\n    boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;\n    return new Id3Header(majorVersion, isUnsynchronized, framesSize);\n  }\n\n  private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion,\n      int frameHeaderSize, boolean unsignedIntFrameSizeHack) {\n    int startPosition = id3Data.getPosition();\n    try {\n      while (id3Data.bytesLeft() >= frameHeaderSize) {\n        // Read the next frame header.\n        int id;\n        long frameSize;\n        int flags;\n        if (majorVersion >= 3) {\n          id = id3Data.readInt();\n          frameSize = id3Data.readUnsignedInt();\n          flags = id3Data.readUnsignedShort();\n        } else {\n          id = id3Data.readUnsignedInt24();\n          frameSize = id3Data.readUnsignedInt24();\n          flags = 0;\n        }\n        // Validate the frame header and skip to the next one.\n        if (id == 0 && frameSize == 0 && flags == 0) {\n          // We've reached zero padding after the end of the final frame.\n          return true;\n        } else {\n          if (majorVersion == 4 && !unsignedIntFrameSizeHack) {\n            // Parse the data size as a synchsafe integer, as per the spec.\n            if ((frameSize & 0x808080L) != 0) {\n              return false;\n            }\n            frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)\n                | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);\n          }\n          boolean hasGroupIdentifier = false;\n          boolean hasDataLength = false;\n          if (majorVersion == 4) {\n            hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;\n            hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;\n          } else if (majorVersion == 3) {\n            hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;\n            // A V3 frame has data length if and only if it's compressed.\n            hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;\n          }\n          int minimumFrameSize = 0;\n          if (hasGroupIdentifier) {\n            minimumFrameSize++;\n          }\n          if (hasDataLength) {\n            minimumFrameSize += 4;\n          }\n          if (frameSize < minimumFrameSize) {\n            return false;\n          }\n          if (id3Data.bytesLeft() < frameSize) {\n            return false;\n          }\n          id3Data.skipBytes((int) frameSize); // flags\n        }\n      }\n      return true;\n    } finally {\n      id3Data.setPosition(startPosition);\n    }\n  }\n\n  @Nullable\n  private static Id3Frame decodeFrame(\n      int majorVersion,\n      ParsableByteArray id3Data,\n      boolean unsignedIntFrameSizeHack,\n      int frameHeaderSize,\n      @Nullable FramePredicate framePredicate) {\n    int frameId0 = id3Data.readUnsignedByte();\n    int frameId1 = id3Data.readUnsignedByte();\n    int frameId2 = id3Data.readUnsignedByte();\n    int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;\n\n    int frameSize;\n    if (majorVersion == 4) {\n      frameSize = id3Data.readUnsignedIntToInt();\n      if (!unsignedIntFrameSizeHack) {\n        frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)\n            | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);\n      }\n    } else if (majorVersion == 3) {\n      frameSize = id3Data.readUnsignedIntToInt();\n    } else /* id3Header.majorVersion == 2 */ {\n      frameSize = id3Data.readUnsignedInt24();\n    }\n\n    int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0;\n    if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0\n        && flags == 0) {\n      // We must be reading zero padding at the end of the tag.\n      id3Data.setPosition(id3Data.limit());\n      return null;\n    }\n\n    int nextFramePosition = id3Data.getPosition() + frameSize;\n    if (nextFramePosition > id3Data.limit()) {\n      Log.w(TAG, \"Frame size exceeds remaining tag data\");\n      id3Data.setPosition(id3Data.limit());\n      return null;\n    }\n\n    if (framePredicate != null\n        && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) {\n      // Filtered by the predicate.\n      id3Data.setPosition(nextFramePosition);\n      return null;\n    }\n\n    // Frame flags.\n    boolean isCompressed = false;\n    boolean isEncrypted = false;\n    boolean isUnsynchronized = false;\n    boolean hasDataLength = false;\n    boolean hasGroupIdentifier = false;\n    if (majorVersion == 3) {\n      isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;\n      isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0;\n      hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;\n      // A V3 frame has data length if and only if it's compressed.\n      hasDataLength = isCompressed;\n    } else if (majorVersion == 4) {\n      hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;\n      isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0;\n      isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0;\n      isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0;\n      hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;\n    }\n\n    if (isCompressed || isEncrypted) {\n      Log.w(TAG, \"Skipping unsupported compressed or encrypted frame\");\n      id3Data.setPosition(nextFramePosition);\n      return null;\n    }\n\n    if (hasGroupIdentifier) {\n      frameSize--;\n      id3Data.skipBytes(1);\n    }\n    if (hasDataLength) {\n      frameSize -= 4;\n      id3Data.skipBytes(4);\n    }\n    if (isUnsynchronized) {\n      frameSize = removeUnsynchronization(id3Data, frameSize);\n    }\n\n    try {\n      Id3Frame frame;\n      if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'\n          && (majorVersion == 2 || frameId3 == 'X')) {\n        frame = decodeTxxxFrame(id3Data, frameSize);\n      } else if (frameId0 == 'T') {\n        String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);\n        frame = decodeTextInformationFrame(id3Data, frameSize, id);\n      } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X'\n          && (majorVersion == 2 || frameId3 == 'X')) {\n        frame = decodeWxxxFrame(id3Data, frameSize);\n      } else if (frameId0 == 'W') {\n        String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);\n        frame = decodeUrlLinkFrame(id3Data, frameSize, id);\n      } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {\n        frame = decodePrivFrame(id3Data, frameSize);\n      } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'\n          && (frameId3 == 'B' || majorVersion == 2)) {\n        frame = decodeGeobFrame(id3Data, frameSize);\n      } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')\n          : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {\n        frame = decodeApicFrame(id3Data, frameSize, majorVersion);\n      } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'\n          && (frameId3 == 'M' || majorVersion == 2)) {\n        frame = decodeCommentFrame(id3Data, frameSize);\n      } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {\n        frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,\n            frameHeaderSize, framePredicate);\n      } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {\n        frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,\n            frameHeaderSize, framePredicate);\n      } else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') {\n        frame = decodeMlltFrame(id3Data, frameSize);\n      } else {\n        String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);\n        frame = decodeBinaryFrame(id3Data, frameSize, id);\n      }\n      if (frame == null) {\n        Log.w(TAG, \"Failed to decode frame: id=\"\n            + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + \", frameSize=\"\n            + frameSize);\n      }\n      return frame;\n    } catch (UnsupportedEncodingException e) {\n      Log.w(TAG, \"Unsupported character encoding\");\n      return null;\n    } finally {\n      id3Data.setPosition(nextFramePosition);\n    }\n  }\n\n  @Nullable\n  private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)\n      throws UnsupportedEncodingException {\n    if (frameSize < 1) {\n      // Frame is malformed.\n      return null;\n    }\n\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[frameSize - 1];\n    id3Data.readBytes(data, 0, frameSize - 1);\n\n    int descriptionEndIndex = indexOfEos(data, 0, encoding);\n    String description = new String(data, 0, descriptionEndIndex, charset);\n\n    int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);\n    int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);\n    String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);\n\n    return new TextInformationFrame(\"TXXX\", description, value);\n  }\n\n  @Nullable\n  private static TextInformationFrame decodeTextInformationFrame(\n      ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException {\n    if (frameSize < 1) {\n      // Frame is malformed.\n      return null;\n    }\n\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[frameSize - 1];\n    id3Data.readBytes(data, 0, frameSize - 1);\n\n    int valueEndIndex = indexOfEos(data, 0, encoding);\n    String value = new String(data, 0, valueEndIndex, charset);\n\n    return new TextInformationFrame(id, null, value);\n  }\n\n  @Nullable\n  private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)\n      throws UnsupportedEncodingException {\n    if (frameSize < 1) {\n      // Frame is malformed.\n      return null;\n    }\n\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[frameSize - 1];\n    id3Data.readBytes(data, 0, frameSize - 1);\n\n    int descriptionEndIndex = indexOfEos(data, 0, encoding);\n    String description = new String(data, 0, descriptionEndIndex, charset);\n\n    int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);\n    int urlEndIndex = indexOfZeroByte(data, urlStartIndex);\n    String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, \"ISO-8859-1\");\n\n    return new UrlLinkFrame(\"WXXX\", description, url);\n  }\n\n  private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,\n      String id) throws UnsupportedEncodingException {\n    byte[] data = new byte[frameSize];\n    id3Data.readBytes(data, 0, frameSize);\n\n    int urlEndIndex = indexOfZeroByte(data, 0);\n    String url = new String(data, 0, urlEndIndex, \"ISO-8859-1\");\n\n    return new UrlLinkFrame(id, null, url);\n  }\n\n  private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize)\n      throws UnsupportedEncodingException {\n    byte[] data = new byte[frameSize];\n    id3Data.readBytes(data, 0, frameSize);\n\n    int ownerEndIndex = indexOfZeroByte(data, 0);\n    String owner = new String(data, 0, ownerEndIndex, \"ISO-8859-1\");\n\n    int privateDataStartIndex = ownerEndIndex + 1;\n    byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length);\n\n    return new PrivFrame(owner, privateData);\n  }\n\n  private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize)\n      throws UnsupportedEncodingException {\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[frameSize - 1];\n    id3Data.readBytes(data, 0, frameSize - 1);\n\n    int mimeTypeEndIndex = indexOfZeroByte(data, 0);\n    String mimeType = new String(data, 0, mimeTypeEndIndex, \"ISO-8859-1\");\n\n    int filenameStartIndex = mimeTypeEndIndex + 1;\n    int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);\n    String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset);\n\n    int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);\n    int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);\n    String description =\n        decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset);\n\n    int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);\n    byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);\n\n    return new GeobFrame(mimeType, filename, description, objectData);\n  }\n\n  private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,\n      int majorVersion) throws UnsupportedEncodingException {\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[frameSize - 1];\n    id3Data.readBytes(data, 0, frameSize - 1);\n\n    String mimeType;\n    int mimeTypeEndIndex;\n    if (majorVersion == 2) {\n      mimeTypeEndIndex = 2;\n      mimeType = \"image/\" + Util.toLowerInvariant(new String(data, 0, 3, \"ISO-8859-1\"));\n      if (\"image/jpg\".equals(mimeType)) {\n        mimeType = \"image/jpeg\";\n      }\n    } else {\n      mimeTypeEndIndex = indexOfZeroByte(data, 0);\n      mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, \"ISO-8859-1\"));\n      if (mimeType.indexOf('/') == -1) {\n        mimeType = \"image/\" + mimeType;\n      }\n    }\n\n    int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;\n\n    int descriptionStartIndex = mimeTypeEndIndex + 2;\n    int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);\n    String description = new String(data, descriptionStartIndex,\n        descriptionEndIndex - descriptionStartIndex, charset);\n\n    int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);\n    byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length);\n\n    return new ApicFrame(mimeType, description, pictureType, pictureData);\n  }\n\n  @Nullable\n  private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)\n      throws UnsupportedEncodingException {\n    if (frameSize < 4) {\n      // Frame is malformed.\n      return null;\n    }\n\n    int encoding = id3Data.readUnsignedByte();\n    String charset = getCharsetName(encoding);\n\n    byte[] data = new byte[3];\n    id3Data.readBytes(data, 0, 3);\n    String language = new String(data, 0, 3);\n\n    data = new byte[frameSize - 4];\n    id3Data.readBytes(data, 0, frameSize - 4);\n\n    int descriptionEndIndex = indexOfEos(data, 0, encoding);\n    String description = new String(data, 0, descriptionEndIndex, charset);\n\n    int textStartIndex = descriptionEndIndex + delimiterLength(encoding);\n    int textEndIndex = indexOfEos(data, textStartIndex, encoding);\n    String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset);\n\n    return new CommentFrame(language, description, text);\n  }\n\n  private static ChapterFrame decodeChapterFrame(\n      ParsableByteArray id3Data,\n      int frameSize,\n      int majorVersion,\n      boolean unsignedIntFrameSizeHack,\n      int frameHeaderSize,\n      @Nullable FramePredicate framePredicate)\n      throws UnsupportedEncodingException {\n    int framePosition = id3Data.getPosition();\n    int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);\n    String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,\n        \"ISO-8859-1\");\n    id3Data.setPosition(chapterIdEndIndex + 1);\n\n    int startTime = id3Data.readInt();\n    int endTime = id3Data.readInt();\n    long startOffset = id3Data.readUnsignedInt();\n    if (startOffset == 0xFFFFFFFFL) {\n      startOffset = C.POSITION_UNSET;\n    }\n    long endOffset = id3Data.readUnsignedInt();\n    if (endOffset == 0xFFFFFFFFL) {\n      endOffset = C.POSITION_UNSET;\n    }\n\n    ArrayList<Id3Frame> subFrames = new ArrayList<>();\n    int limit = framePosition + frameSize;\n    while (id3Data.getPosition() < limit) {\n      Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,\n          frameHeaderSize, framePredicate);\n      if (frame != null) {\n        subFrames.add(frame);\n      }\n    }\n\n    Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];\n    subFrames.toArray(subFrameArray);\n    return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);\n  }\n\n  private static ChapterTocFrame decodeChapterTOCFrame(\n      ParsableByteArray id3Data,\n      int frameSize,\n      int majorVersion,\n      boolean unsignedIntFrameSizeHack,\n      int frameHeaderSize,\n      @Nullable FramePredicate framePredicate)\n      throws UnsupportedEncodingException {\n    int framePosition = id3Data.getPosition();\n    int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);\n    String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition,\n        \"ISO-8859-1\");\n    id3Data.setPosition(elementIdEndIndex + 1);\n\n    int ctocFlags = id3Data.readUnsignedByte();\n    boolean isRoot = (ctocFlags & 0x0002) != 0;\n    boolean isOrdered = (ctocFlags & 0x0001) != 0;\n\n    int childCount = id3Data.readUnsignedByte();\n    String[] children = new String[childCount];\n    for (int i = 0; i < childCount; i++) {\n      int startIndex = id3Data.getPosition();\n      int endIndex = indexOfZeroByte(id3Data.data, startIndex);\n      children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, \"ISO-8859-1\");\n      id3Data.setPosition(endIndex + 1);\n    }\n\n    ArrayList<Id3Frame> subFrames = new ArrayList<>();\n    int limit = framePosition + frameSize;\n    while (id3Data.getPosition() < limit) {\n      Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,\n          frameHeaderSize, framePredicate);\n      if (frame != null) {\n        subFrames.add(frame);\n      }\n    }\n\n    Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];\n    subFrames.toArray(subFrameArray);\n    return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray);\n  }\n\n  private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) {\n    // See ID3v2.4.0 native frames subsection 4.6.\n    int mpegFramesBetweenReference = id3Data.readUnsignedShort();\n    int bytesBetweenReference = id3Data.readUnsignedInt24();\n    int millisecondsBetweenReference = id3Data.readUnsignedInt24();\n    int bitsForBytesDeviation = id3Data.readUnsignedByte();\n    int bitsForMillisecondsDeviation = id3Data.readUnsignedByte();\n\n    ParsableBitArray references = new ParsableBitArray();\n    references.reset(id3Data);\n    int referencesBits = 8 * (frameSize - 10);\n    int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation;\n    int referencesCount = referencesBits / bitsPerReference;\n    int[] bytesDeviations = new int[referencesCount];\n    int[] millisecondsDeviations = new int[referencesCount];\n    for (int i = 0; i < referencesCount; i++) {\n      int bytesDeviation = references.readBits(bitsForBytesDeviation);\n      int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation);\n      bytesDeviations[i] = bytesDeviation;\n      millisecondsDeviations[i] = millisecondsDeviation;\n    }\n\n    return new MlltFrame(\n        mpegFramesBetweenReference,\n        bytesBetweenReference,\n        millisecondsBetweenReference,\n        bytesDeviations,\n        millisecondsDeviations);\n  }\n\n  private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,\n      String id) {\n    byte[] frame = new byte[frameSize];\n    id3Data.readBytes(frame, 0, frameSize);\n\n    return new BinaryFrame(id, frame);\n  }\n\n  /**\n   * Performs in-place removal of unsynchronization for {@code length} bytes starting from\n   * {@link ParsableByteArray#getPosition()}\n   *\n   * @param data Contains the data to be processed.\n   * @param length The length of the data to be processed.\n   * @return The length of the data after processing.\n   */\n  private static int removeUnsynchronization(ParsableByteArray data, int length) {\n    byte[] bytes = data.data;\n    int startPosition = data.getPosition();\n    for (int i = startPosition; i + 1 < startPosition + length; i++) {\n      if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {\n        int relativePosition = i - startPosition;\n        System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2);\n        length--;\n      }\n    }\n    return length;\n  }\n\n  /**\n   * Maps encoding byte from ID3v2 frame to a Charset.\n   *\n   * @param encodingByte The value of encoding byte from ID3v2 frame.\n   * @return Charset name.\n   */\n  private static String getCharsetName(int encodingByte) {\n    switch (encodingByte) {\n      case ID3_TEXT_ENCODING_UTF_16:\n        return \"UTF-16\";\n      case ID3_TEXT_ENCODING_UTF_16BE:\n        return \"UTF-16BE\";\n      case ID3_TEXT_ENCODING_UTF_8:\n        return \"UTF-8\";\n      case ID3_TEXT_ENCODING_ISO_8859_1:\n      default:\n        return \"ISO-8859-1\";\n    }\n  }\n\n  private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2,\n      int frameId3) {\n    return majorVersion == 2 ? String.format(Locale.US, \"%c%c%c\", frameId0, frameId1, frameId2)\n        : String.format(Locale.US, \"%c%c%c%c\", frameId0, frameId1, frameId2, frameId3);\n  }\n\n  private static int indexOfEos(byte[] data, int fromIndex, int encoding) {\n    int terminationPos = indexOfZeroByte(data, fromIndex);\n\n    // For single byte encoding charsets, we're done.\n    if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {\n      return terminationPos;\n    }\n\n    // Otherwise ensure an even index and look for a second zero byte.\n    while (terminationPos < data.length - 1) {\n      if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {\n        return terminationPos;\n      }\n      terminationPos = indexOfZeroByte(data, terminationPos + 1);\n    }\n\n    return data.length;\n  }\n\n  private static int indexOfZeroByte(byte[] data, int fromIndex) {\n    for (int i = fromIndex; i < data.length; i++) {\n      if (data[i] == (byte) 0) {\n        return i;\n      }\n    }\n    return data.length;\n  }\n\n  private static int delimiterLength(int encodingByte) {\n    return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)\n        ? 1 : 2;\n  }\n\n  /**\n   * Copies the specified range of an array, or returns a zero length array if the range is invalid.\n   *\n   * @param data The array from which to copy.\n   * @param from The start of the range to copy (inclusive).\n   * @param to The end of the range to copy (exclusive).\n   * @return The copied data, or a zero length array if the range is invalid.\n   */\n  private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) {\n    if (to <= from) {\n      // Invalid or zero length range.\n      return Util.EMPTY_BYTE_ARRAY;\n    }\n    return Arrays.copyOfRange(data, from, to);\n  }\n\n  /**\n   * Returns a string obtained by decoding the specified range of {@code data} using the specified\n   * {@code charsetName}. An empty string is returned if the range is invalid.\n   *\n   * @param data The array from which to decode the string.\n   * @param from The start of the range.\n   * @param to The end of the range (exclusive).\n   * @param charsetName The name of the Charset to use.\n   * @return The decoded string, or an empty string if the range is invalid.\n   * @throws UnsupportedEncodingException If the Charset is not supported.\n   */\n  private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName)\n      throws UnsupportedEncodingException {\n    if (to <= from || to > data.length) {\n      return \"\";\n    }\n    return new String(data, from, to - from, charsetName);\n  }\n\n  private static final class Id3Header {\n\n    private final int majorVersion;\n    private final boolean isUnsynchronized;\n    private final int framesSize;\n\n    public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) {\n      this.majorVersion = majorVersion;\n      this.isUnsynchronized = isUnsynchronized;\n      this.framesSize = framesSize;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport com.google.android.exoplayer2.metadata.Metadata;\n\n/**\n * Base class for ID3 frames.\n */\npublic abstract class Id3Frame implements Metadata.Entry {\n\n  /**\n   * The frame ID.\n   */\n  public final String id;\n\n  public Id3Frame(String id) {\n    this.id = id;\n  }\n\n  @Override\n  public String toString() {\n    return id;\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Internal ID3 frame that is intended for use by the player. */\npublic final class InternalFrame extends Id3Frame {\n\n  public static final String ID = \"----\";\n\n  public final String domain;\n  public final String description;\n  public final String text;\n\n  public InternalFrame(String domain, String description, String text) {\n    super(ID);\n    this.domain = domain;\n    this.description = description;\n    this.text = text;\n  }\n\n  /* package */ InternalFrame(Parcel in) {\n    super(ID);\n    domain = castNonNull(in.readString());\n    description = castNonNull(in.readString());\n    text = castNonNull(in.readString());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    InternalFrame other = (InternalFrame) obj;\n    return Util.areEqual(description, other.description)\n        && Util.areEqual(domain, other.domain)\n        && Util.areEqual(text, other.text);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (domain != null ? domain.hashCode() : 0);\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + (text != null ? text.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": domain=\" + domain + \", description=\" + description;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(domain);\n    dest.writeString(text);\n  }\n\n  public static final Creator<InternalFrame> CREATOR =\n      new Creator<InternalFrame>() {\n\n        @Override\n        public InternalFrame createFromParcel(Parcel in) {\n          return new InternalFrame(in);\n        }\n\n        @Override\n        public InternalFrame[] newArray(int size) {\n          return new InternalFrame[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/MlltFrame.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport android.os.Parcel;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/** MPEG location lookup table frame. */\npublic final class MlltFrame extends Id3Frame {\n\n  public static final String ID = \"MLLT\";\n\n  public final int mpegFramesBetweenReference;\n  public final int bytesBetweenReference;\n  public final int millisecondsBetweenReference;\n  public final int[] bytesDeviations;\n  public final int[] millisecondsDeviations;\n\n  public MlltFrame(\n      int mpegFramesBetweenReference,\n      int bytesBetweenReference,\n      int millisecondsBetweenReference,\n      int[] bytesDeviations,\n      int[] millisecondsDeviations) {\n    super(ID);\n    this.mpegFramesBetweenReference = mpegFramesBetweenReference;\n    this.bytesBetweenReference = bytesBetweenReference;\n    this.millisecondsBetweenReference = millisecondsBetweenReference;\n    this.bytesDeviations = bytesDeviations;\n    this.millisecondsDeviations = millisecondsDeviations;\n  }\n\n  /* package */\n  MlltFrame(Parcel in) {\n    super(ID);\n    this.mpegFramesBetweenReference = in.readInt();\n    this.bytesBetweenReference = in.readInt();\n    this.millisecondsBetweenReference = in.readInt();\n    this.bytesDeviations = Util.castNonNull(in.createIntArray());\n    this.millisecondsDeviations = Util.castNonNull(in.createIntArray());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    MlltFrame other = (MlltFrame) obj;\n    return mpegFramesBetweenReference == other.mpegFramesBetweenReference\n        && bytesBetweenReference == other.bytesBetweenReference\n        && millisecondsBetweenReference == other.millisecondsBetweenReference\n        && Arrays.equals(bytesDeviations, other.bytesDeviations)\n        && Arrays.equals(millisecondsDeviations, other.millisecondsDeviations);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + mpegFramesBetweenReference;\n    result = 31 * result + bytesBetweenReference;\n    result = 31 * result + millisecondsBetweenReference;\n    result = 31 * result + Arrays.hashCode(bytesDeviations);\n    result = 31 * result + Arrays.hashCode(millisecondsDeviations);\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(mpegFramesBetweenReference);\n    dest.writeInt(bytesBetweenReference);\n    dest.writeInt(millisecondsBetweenReference);\n    dest.writeIntArray(bytesDeviations);\n    dest.writeIntArray(millisecondsDeviations);\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Creator<MlltFrame> CREATOR =\n      new Creator<MlltFrame>() {\n\n        @Override\n        public MlltFrame createFromParcel(Parcel in) {\n          return new MlltFrame(in);\n        }\n\n        @Override\n        public MlltFrame[] newArray(int size) {\n          return new MlltFrame[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * PRIV (Private) ID3 frame.\n */\npublic final class PrivFrame extends Id3Frame {\n\n  public static final String ID = \"PRIV\";\n\n  public final String owner;\n  public final byte[] privateData;\n\n  public PrivFrame(String owner, byte[] privateData) {\n    super(ID);\n    this.owner = owner;\n    this.privateData = privateData;\n  }\n\n  /* package */ PrivFrame(Parcel in) {\n    super(ID);\n    owner = castNonNull(in.readString());\n    privateData = castNonNull(in.createByteArray());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    PrivFrame other = (PrivFrame) obj;\n    return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (owner != null ? owner.hashCode() : 0);\n    result = 31 * result + Arrays.hashCode(privateData);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": owner=\" + owner;\n  }\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(owner);\n    dest.writeByteArray(privateData);\n  }\n\n  public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() {\n\n    @Override\n    public PrivFrame createFromParcel(Parcel in) {\n      return new PrivFrame(in);\n    }\n\n    @Override\n    public PrivFrame[] newArray(int size) {\n      return new PrivFrame[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Text information ID3 frame.\n */\npublic final class TextInformationFrame extends Id3Frame {\n\n  @Nullable public final String description;\n  public final String value;\n\n  public TextInformationFrame(String id, @Nullable String description, String value) {\n    super(id);\n    this.description = description;\n    this.value = value;\n  }\n\n  /* package */ TextInformationFrame(Parcel in) {\n    super(castNonNull(in.readString()));\n    description = in.readString();\n    value = castNonNull(in.readString());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    TextInformationFrame other = (TextInformationFrame) obj;\n    return id.equals(other.id) && Util.areEqual(description, other.description)\n        && Util.areEqual(value, other.value);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + id.hashCode();\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + (value != null ? value.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": description=\" + description + \": value=\" + value;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(description);\n    dest.writeString(value);\n  }\n\n  public static final Parcelable.Creator<TextInformationFrame> CREATOR =\n      new Parcelable.Creator<TextInformationFrame>() {\n\n        @Override\n        public TextInformationFrame createFromParcel(Parcel in) {\n          return new TextInformationFrame(in);\n        }\n\n        @Override\n        public TextInformationFrame[] newArray(int size) {\n          return new TextInformationFrame[size];\n        }\n\n      };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Url link ID3 frame.\n */\npublic final class UrlLinkFrame extends Id3Frame {\n\n  @Nullable public final String description;\n  public final String url;\n\n  public UrlLinkFrame(String id, @Nullable String description, String url) {\n    super(id);\n    this.description = description;\n    this.url = url;\n  }\n\n  /* package */ UrlLinkFrame(Parcel in) {\n    super(castNonNull(in.readString()));\n    description = in.readString();\n    url = castNonNull(in.readString());\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    UrlLinkFrame other = (UrlLinkFrame) obj;\n    return id.equals(other.id) && Util.areEqual(description, other.description)\n        && Util.areEqual(url, other.url);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + id.hashCode();\n    result = 31 * result + (description != null ? description.hashCode() : 0);\n    result = 31 * result + (url != null ? url.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return id + \": url=\" + url;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(description);\n    dest.writeString(url);\n  }\n\n  public static final Parcelable.Creator<UrlLinkFrame> CREATOR =\n      new Parcelable.Creator<UrlLinkFrame>() {\n\n        @Override\n        public UrlLinkFrame createFromParcel(Parcel in) {\n          return new UrlLinkFrame(in);\n        }\n\n        @Override\n        public UrlLinkFrame[] newArray(int size) {\n          return new UrlLinkFrame[size];\n        }\n\n      };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata.id3;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/**\n * Represents a private command as defined in SCTE35, Section 9.3.6.\n */\npublic final class PrivateCommand extends SpliceCommand {\n\n  /**\n   * The {@code pts_adjustment} as defined in SCTE35, Section 9.2.\n   */\n  public final long ptsAdjustment;\n  /**\n   * The identifier as defined in SCTE35, Section 9.3.6.\n   */\n  public final long identifier;\n  /**\n   * The private bytes as defined in SCTE35, Section 9.3.6.\n   */\n  public final byte[] commandBytes;\n\n  private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) {\n    this.ptsAdjustment = ptsAdjustment;\n    this.identifier = identifier;\n    this.commandBytes = commandBytes;\n  }\n\n  private PrivateCommand(Parcel in) {\n    ptsAdjustment = in.readLong();\n    identifier = in.readLong();\n    commandBytes = new byte[in.readInt()];\n    in.readByteArray(commandBytes);\n  }\n\n  /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData,\n      int commandLength, long ptsAdjustment) {\n    long identifier = sectionData.readUnsignedInt();\n    byte[] privateBytes = new byte[commandLength - 4 /* identifier size */];\n    sectionData.readBytes(privateBytes, 0, privateBytes.length);\n    return new PrivateCommand(identifier, privateBytes, ptsAdjustment);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeLong(ptsAdjustment);\n    dest.writeLong(identifier);\n    dest.writeInt(commandBytes.length);\n    dest.writeByteArray(commandBytes);\n  }\n\n  public static final Parcelable.Creator<PrivateCommand> CREATOR =\n      new Parcelable.Creator<PrivateCommand>() {\n\n    @Override\n    public PrivateCommand createFromParcel(Parcel in) {\n      return new PrivateCommand(in);\n    }\n\n    @Override\n    public PrivateCommand[] newArray(int size) {\n      return new PrivateCommand[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport com.google.android.exoplayer2.metadata.Metadata;\n\n/**\n * Superclass for SCTE35 splice commands.\n */\npublic abstract class SpliceCommand implements Metadata.Entry {\n\n  @Override\n  public String toString() {\n    return \"SCTE-35 splice command: type=\" + getClass().getSimpleName();\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataDecoder;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.nio.ByteBuffer;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * Decodes splice info sections and produces splice commands.\n */\npublic final class SpliceInfoDecoder implements MetadataDecoder {\n\n  private static final int TYPE_SPLICE_NULL = 0x00;\n  private static final int TYPE_SPLICE_SCHEDULE = 0x04;\n  private static final int TYPE_SPLICE_INSERT = 0x05;\n  private static final int TYPE_TIME_SIGNAL = 0x06;\n  private static final int TYPE_PRIVATE_COMMAND = 0xFF;\n\n  private final ParsableByteArray sectionData;\n  private final ParsableBitArray sectionHeader;\n\n  @MonotonicNonNull private TimestampAdjuster timestampAdjuster;\n\n  public SpliceInfoDecoder() {\n    sectionData = new ParsableByteArray();\n    sectionHeader = new ParsableBitArray();\n  }\n\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  @Override\n  public Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);\n\n    // Internal timestamps adjustment.\n    if (timestampAdjuster == null\n        || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {\n      timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);\n      timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);\n    }\n\n    byte[] data = buffer.array();\n    int size = buffer.limit();\n    sectionData.reset(data, size);\n    sectionHeader.reset(data, size);\n    // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),\n    // section_length(12), protocol_version(8), encrypted_packet(1), encryption_algorithm(6).\n    sectionHeader.skipBits(39);\n    long ptsAdjustment = sectionHeader.readBits(1);\n    ptsAdjustment = (ptsAdjustment << 32) | sectionHeader.readBits(32);\n    // cw_index(8), tier(12).\n    sectionHeader.skipBits(20);\n    int spliceCommandLength = sectionHeader.readBits(12);\n    int spliceCommandType = sectionHeader.readBits(8);\n    @Nullable SpliceCommand command = null;\n    // Go to the start of the command by skipping all fields up to command_type.\n    sectionData.skipBytes(14);\n    switch (spliceCommandType) {\n      case TYPE_SPLICE_NULL:\n        command = new SpliceNullCommand();\n        break;\n      case TYPE_SPLICE_SCHEDULE:\n        command = SpliceScheduleCommand.parseFromSection(sectionData);\n        break;\n      case TYPE_SPLICE_INSERT:\n        command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment,\n            timestampAdjuster);\n        break;\n      case TYPE_TIME_SIGNAL:\n        command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster);\n        break;\n      case TYPE_PRIVATE_COMMAND:\n        command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment);\n        break;\n      default:\n        // Do nothing.\n        break;\n    }\n    return command == null ? new Metadata() : new Metadata(command);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Represents a splice insert command defined in SCTE35, Section 9.3.3.\n */\npublic final class SpliceInsertCommand extends SpliceCommand {\n\n  /**\n   * The splice event id.\n   */\n  public final long spliceEventId;\n  /**\n   * True if the event with id {@link #spliceEventId} has been canceled.\n   */\n  public final boolean spliceEventCancelIndicator;\n  /**\n   * If true, the splice event is an opportunity to exit from the network feed. If false, indicates\n   * an opportunity to return to the network feed.\n   */\n  public final boolean outOfNetworkIndicator;\n  /**\n   * Whether the splice mode is program splice mode, whereby all PIDs/components are to be spliced.\n   * If false, splicing is done per PID/component.\n   */\n  public final boolean programSpliceFlag;\n  /**\n   * Whether splicing should be done at the nearest opportunity. If false, splicing should be done\n   * at the moment indicated by {@link #programSplicePlaybackPositionUs} or\n   * {@link ComponentSplice#componentSplicePlaybackPositionUs}, depending on\n   * {@link #programSpliceFlag}.\n   */\n  public final boolean spliceImmediateFlag;\n  /**\n   * If {@link #programSpliceFlag} is true, the PTS at which the program splice should occur.\n   * {@link C#TIME_UNSET} otherwise.\n   */\n  public final long programSplicePts;\n  /**\n   * Equivalent to {@link #programSplicePts} but in the playback timebase.\n   */\n  public final long programSplicePlaybackPositionUs;\n  /**\n   * If {@link #programSpliceFlag} is false, a non-empty list containing the\n   * {@link ComponentSplice}s. Otherwise, an empty list.\n   */\n  public final List<ComponentSplice> componentSpliceList;\n  /**\n   * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether\n   * {@link #breakDurationUs} should be used to know when to return to the network feed. If\n   * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined.\n   */\n  public final boolean autoReturn;\n  /**\n   * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is present.\n   */\n  public final long breakDurationUs;\n  /**\n   * The unique program id as defined in SCTE35, Section 9.3.3.\n   */\n  public final int uniqueProgramId;\n  /**\n   * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.3.\n   */\n  public final int availNum;\n  /**\n   * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.3.\n   */\n  public final int availsExpected;\n\n  private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator,\n      boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag,\n      long programSplicePts, long programSplicePlaybackPositionUs,\n      List<ComponentSplice> componentSpliceList, boolean autoReturn, long breakDurationUs,\n      int uniqueProgramId, int availNum, int availsExpected) {\n    this.spliceEventId = spliceEventId;\n    this.spliceEventCancelIndicator = spliceEventCancelIndicator;\n    this.outOfNetworkIndicator = outOfNetworkIndicator;\n    this.programSpliceFlag = programSpliceFlag;\n    this.spliceImmediateFlag = spliceImmediateFlag;\n    this.programSplicePts = programSplicePts;\n    this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs;\n    this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);\n    this.autoReturn = autoReturn;\n    this.breakDurationUs = breakDurationUs;\n    this.uniqueProgramId = uniqueProgramId;\n    this.availNum = availNum;\n    this.availsExpected = availsExpected;\n  }\n\n  private SpliceInsertCommand(Parcel in) {\n    spliceEventId = in.readLong();\n    spliceEventCancelIndicator = in.readByte() == 1;\n    outOfNetworkIndicator = in.readByte() == 1;\n    programSpliceFlag = in.readByte() == 1;\n    spliceImmediateFlag = in.readByte() == 1;\n    programSplicePts = in.readLong();\n    programSplicePlaybackPositionUs = in.readLong();\n    int componentSpliceListSize = in.readInt();\n    List<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListSize);\n    for (int i = 0; i < componentSpliceListSize; i++) {\n      componentSpliceList.add(ComponentSplice.createFromParcel(in));\n    }\n    this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);\n    autoReturn = in.readByte() == 1;\n    breakDurationUs = in.readLong();\n    uniqueProgramId = in.readInt();\n    availNum = in.readInt();\n    availsExpected = in.readInt();\n  }\n\n  /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData,\n      long ptsAdjustment, TimestampAdjuster timestampAdjuster) {\n    long spliceEventId = sectionData.readUnsignedInt();\n    // splice_event_cancel_indicator(1), reserved(7).\n    boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;\n    boolean outOfNetworkIndicator = false;\n    boolean programSpliceFlag = false;\n    boolean spliceImmediateFlag = false;\n    long programSplicePts = C.TIME_UNSET;\n    List<ComponentSplice> componentSplices = Collections.emptyList();\n    int uniqueProgramId = 0;\n    int availNum = 0;\n    int availsExpected = 0;\n    boolean autoReturn = false;\n    long breakDurationUs = C.TIME_UNSET;\n    if (!spliceEventCancelIndicator) {\n      int headerByte = sectionData.readUnsignedByte();\n      outOfNetworkIndicator = (headerByte & 0x80) != 0;\n      programSpliceFlag = (headerByte & 0x40) != 0;\n      boolean durationFlag = (headerByte & 0x20) != 0;\n      spliceImmediateFlag = (headerByte & 0x10) != 0;\n      if (programSpliceFlag && !spliceImmediateFlag) {\n        programSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);\n      }\n      if (!programSpliceFlag) {\n        int componentCount = sectionData.readUnsignedByte();\n        componentSplices = new ArrayList<>(componentCount);\n        for (int i = 0; i < componentCount; i++) {\n          int componentTag = sectionData.readUnsignedByte();\n          long componentSplicePts = C.TIME_UNSET;\n          if (!spliceImmediateFlag) {\n            componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);\n          }\n          componentSplices.add(new ComponentSplice(componentTag, componentSplicePts,\n              timestampAdjuster.adjustTsTimestamp(componentSplicePts)));\n        }\n      }\n      if (durationFlag) {\n        long firstByte = sectionData.readUnsignedByte();\n        autoReturn = (firstByte & 0x80) != 0;\n        long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();\n        breakDurationUs = breakDuration90khz * 1000 / 90;\n      }\n      uniqueProgramId = sectionData.readUnsignedShort();\n      availNum = sectionData.readUnsignedByte();\n      availsExpected = sectionData.readUnsignedByte();\n    }\n    return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,\n        programSpliceFlag, spliceImmediateFlag, programSplicePts,\n        timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,\n        breakDurationUs, uniqueProgramId, availNum, availsExpected);\n  }\n\n  /**\n   * Holds splicing information for specific splice insert command components.\n   */\n  public static final class ComponentSplice {\n\n    public final int componentTag;\n    public final long componentSplicePts;\n    public final long componentSplicePlaybackPositionUs;\n\n    private ComponentSplice(int componentTag, long componentSplicePts,\n        long componentSplicePlaybackPositionUs) {\n      this.componentTag = componentTag;\n      this.componentSplicePts = componentSplicePts;\n      this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs;\n    }\n\n    public void writeToParcel(Parcel dest) {\n      dest.writeInt(componentTag);\n      dest.writeLong(componentSplicePts);\n      dest.writeLong(componentSplicePlaybackPositionUs);\n    }\n\n    public static ComponentSplice createFromParcel(Parcel in) {\n      return new ComponentSplice(in.readInt(), in.readLong(), in.readLong());\n    }\n\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeLong(spliceEventId);\n    dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));\n    dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));\n    dest.writeByte((byte) (programSpliceFlag ? 1 : 0));\n    dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0));\n    dest.writeLong(programSplicePts);\n    dest.writeLong(programSplicePlaybackPositionUs);\n    int componentSpliceListSize = componentSpliceList.size();\n    dest.writeInt(componentSpliceListSize);\n    for (int i = 0; i < componentSpliceListSize; i++) {\n      componentSpliceList.get(i).writeToParcel(dest);\n    }\n    dest.writeByte((byte) (autoReturn ? 1 : 0));\n    dest.writeLong(breakDurationUs);\n    dest.writeInt(uniqueProgramId);\n    dest.writeInt(availNum);\n    dest.writeInt(availsExpected);\n  }\n\n  public static final Parcelable.Creator<SpliceInsertCommand> CREATOR =\n      new Parcelable.Creator<SpliceInsertCommand>() {\n\n    @Override\n    public SpliceInsertCommand createFromParcel(Parcel in) {\n      return new SpliceInsertCommand(in);\n    }\n\n    @Override\n    public SpliceInsertCommand[] newArray(int size) {\n      return new SpliceInsertCommand[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport android.os.Parcel;\n\n/**\n * Represents a splice null command as defined in SCTE35, Section 9.3.1.\n */\npublic final class SpliceNullCommand extends SpliceCommand {\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    // Do nothing.\n  }\n\n  public static final Creator<SpliceNullCommand> CREATOR =\n      new Creator<SpliceNullCommand>() {\n\n    @Override\n    public SpliceNullCommand createFromParcel(Parcel in) {\n      return new SpliceNullCommand();\n    }\n\n    @Override\n    public SpliceNullCommand[] newArray(int size) {\n      return new SpliceNullCommand[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Represents a splice schedule command as defined in SCTE35, Section 9.3.2.\n */\npublic final class SpliceScheduleCommand extends SpliceCommand {\n\n  /**\n   * Represents a splice event as contained in a {@link SpliceScheduleCommand}.\n   */\n  public static final class Event {\n\n    /**\n     * The splice event id.\n     */\n    public final long spliceEventId;\n    /**\n     * True if the event with id {@link #spliceEventId} has been canceled.\n     */\n    public final boolean spliceEventCancelIndicator;\n    /**\n     * If true, the splice event is an opportunity to exit from the network feed. If false,\n     * indicates an opportunity to return to the network feed.\n     */\n    public final boolean outOfNetworkIndicator;\n    /**\n     * Whether the splice mode is program splice mode, whereby all PIDs/components are to be\n     * spliced. If false, splicing is done per PID/component.\n     */\n    public final boolean programSpliceFlag;\n    /**\n     * Represents the time of the signaled splice event as the number of seconds since 00 hours UTC,\n     * January 6th, 1980, with the count of intervening leap seconds included.\n     */\n    public final long utcSpliceTime;\n    /**\n     * If {@link #programSpliceFlag} is false, a non-empty list containing the\n     * {@link ComponentSplice}s. Otherwise, an empty list.\n     */\n    public final List<ComponentSplice> componentSpliceList;\n    /**\n     * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether\n     * {@link #breakDurationUs} should be used to know when to return to the network feed. If\n     * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined.\n     */\n    public final boolean autoReturn;\n    /**\n     * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is\n     * present.\n     */\n    public final long breakDurationUs;\n    /**\n     * The unique program id as defined in SCTE35, Section 9.3.2.\n     */\n    public final int uniqueProgramId;\n    /**\n     * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.2.\n     */\n    public final int availNum;\n    /**\n     * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.2.\n     */\n    public final int availsExpected;\n\n    private Event(long spliceEventId, boolean spliceEventCancelIndicator,\n        boolean outOfNetworkIndicator, boolean programSpliceFlag,\n        List<ComponentSplice> componentSpliceList, long utcSpliceTime, boolean autoReturn,\n        long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) {\n      this.spliceEventId = spliceEventId;\n      this.spliceEventCancelIndicator = spliceEventCancelIndicator;\n      this.outOfNetworkIndicator = outOfNetworkIndicator;\n      this.programSpliceFlag = programSpliceFlag;\n      this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);\n      this.utcSpliceTime = utcSpliceTime;\n      this.autoReturn = autoReturn;\n      this.breakDurationUs = breakDurationUs;\n      this.uniqueProgramId = uniqueProgramId;\n      this.availNum = availNum;\n      this.availsExpected = availsExpected;\n    }\n\n    private Event(Parcel in) {\n      this.spliceEventId = in.readLong();\n      this.spliceEventCancelIndicator = in.readByte() == 1;\n      this.outOfNetworkIndicator = in.readByte() == 1;\n      this.programSpliceFlag = in.readByte() == 1;\n      int componentSpliceListLength = in.readInt();\n      ArrayList<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListLength);\n      for (int i = 0; i < componentSpliceListLength; i++) {\n        componentSpliceList.add(ComponentSplice.createFromParcel(in));\n      }\n      this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);\n      this.utcSpliceTime = in.readLong();\n      this.autoReturn = in.readByte() == 1;\n      this.breakDurationUs = in.readLong();\n      this.uniqueProgramId = in.readInt();\n      this.availNum = in.readInt();\n      this.availsExpected = in.readInt();\n    }\n\n    private static Event parseFromSection(ParsableByteArray sectionData) {\n      long spliceEventId = sectionData.readUnsignedInt();\n      // splice_event_cancel_indicator(1), reserved(7).\n      boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;\n      boolean outOfNetworkIndicator = false;\n      boolean programSpliceFlag = false;\n      long utcSpliceTime = C.TIME_UNSET;\n      ArrayList<ComponentSplice> componentSplices = new ArrayList<>();\n      int uniqueProgramId = 0;\n      int availNum = 0;\n      int availsExpected = 0;\n      boolean autoReturn = false;\n      long breakDurationUs = C.TIME_UNSET;\n      if (!spliceEventCancelIndicator) {\n        int headerByte = sectionData.readUnsignedByte();\n        outOfNetworkIndicator = (headerByte & 0x80) != 0;\n        programSpliceFlag = (headerByte & 0x40) != 0;\n        boolean durationFlag = (headerByte & 0x20) != 0;\n        if (programSpliceFlag) {\n          utcSpliceTime = sectionData.readUnsignedInt();\n        }\n        if (!programSpliceFlag) {\n          int componentCount = sectionData.readUnsignedByte();\n          componentSplices = new ArrayList<>(componentCount);\n          for (int i = 0; i < componentCount; i++) {\n            int componentTag = sectionData.readUnsignedByte();\n            long componentUtcSpliceTime = sectionData.readUnsignedInt();\n            componentSplices.add(new ComponentSplice(componentTag, componentUtcSpliceTime));\n          }\n        }\n        if (durationFlag) {\n          long firstByte = sectionData.readUnsignedByte();\n          autoReturn = (firstByte & 0x80) != 0;\n          long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();\n          breakDurationUs = breakDuration90khz * 1000 / 90;\n        }\n        uniqueProgramId = sectionData.readUnsignedShort();\n        availNum = sectionData.readUnsignedByte();\n        availsExpected = sectionData.readUnsignedByte();\n      }\n      return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,\n          programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, breakDurationUs,\n          uniqueProgramId, availNum, availsExpected);\n    }\n\n    private void writeToParcel(Parcel dest) {\n      dest.writeLong(spliceEventId);\n      dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));\n      dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));\n      dest.writeByte((byte) (programSpliceFlag ? 1 : 0));\n      int componentSpliceListSize = componentSpliceList.size();\n      dest.writeInt(componentSpliceListSize);\n      for (int i = 0; i < componentSpliceListSize; i++) {\n        componentSpliceList.get(i).writeToParcel(dest);\n      }\n      dest.writeLong(utcSpliceTime);\n      dest.writeByte((byte) (autoReturn ? 1 : 0));\n      dest.writeLong(breakDurationUs);\n      dest.writeInt(uniqueProgramId);\n      dest.writeInt(availNum);\n      dest.writeInt(availsExpected);\n    }\n\n    private static Event createFromParcel(Parcel in) {\n      return new Event(in);\n    }\n\n  }\n\n  /**\n   * Holds splicing information for specific splice schedule command components.\n   */\n  public static final class ComponentSplice {\n\n    public final int componentTag;\n    public final long utcSpliceTime;\n\n    private ComponentSplice(int componentTag, long utcSpliceTime) {\n      this.componentTag = componentTag;\n      this.utcSpliceTime = utcSpliceTime;\n    }\n\n    private static ComponentSplice createFromParcel(Parcel in) {\n      return new ComponentSplice(in.readInt(), in.readLong());\n    }\n\n    private void writeToParcel(Parcel dest) {\n      dest.writeInt(componentTag);\n      dest.writeLong(utcSpliceTime);\n    }\n\n  }\n\n  /**\n   * The list of scheduled events.\n   */\n  public final List<Event> events;\n\n  private SpliceScheduleCommand(List<Event> events) {\n    this.events = Collections.unmodifiableList(events);\n  }\n\n  private SpliceScheduleCommand(Parcel in) {\n    int eventsSize = in.readInt();\n    ArrayList<Event> events = new ArrayList<>(eventsSize);\n    for (int i = 0; i < eventsSize; i++) {\n      events.add(Event.createFromParcel(in));\n    }\n    this.events = Collections.unmodifiableList(events);\n  }\n\n  /* package */ static SpliceScheduleCommand parseFromSection(ParsableByteArray sectionData) {\n    int spliceCount = sectionData.readUnsignedByte();\n    ArrayList<Event> events = new ArrayList<>(spliceCount);\n    for (int i = 0; i < spliceCount; i++) {\n      events.add(Event.parseFromSection(sectionData));\n    }\n    return new SpliceScheduleCommand(events);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    int eventsSize = events.size();\n    dest.writeInt(eventsSize);\n    for (int i = 0; i < eventsSize; i++) {\n      events.get(i).writeToParcel(dest);\n    }\n  }\n\n  public static final Parcelable.Creator<SpliceScheduleCommand> CREATOR =\n      new Parcelable.Creator<SpliceScheduleCommand>() {\n\n    @Override\n    public SpliceScheduleCommand createFromParcel(Parcel in) {\n      return new SpliceScheduleCommand(in);\n    }\n\n    @Override\n    public SpliceScheduleCommand[] newArray(int size) {\n      return new SpliceScheduleCommand[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport android.os.Parcel;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\n\n/**\n * Represents a time signal command as defined in SCTE35, Section 9.3.4.\n */\npublic final class TimeSignalCommand extends SpliceCommand {\n\n  /**\n   * A PTS value, as defined in SCTE35, Section 9.3.4.\n   */\n  public final long ptsTime;\n  /**\n   * Equivalent to {@link #ptsTime} but in the playback timebase.\n   */\n  public final long playbackPositionUs;\n\n  private TimeSignalCommand(long ptsTime, long playbackPositionUs) {\n    this.ptsTime = ptsTime;\n    this.playbackPositionUs = playbackPositionUs;\n  }\n\n  /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData,\n      long ptsAdjustment, TimestampAdjuster timestampAdjuster) {\n    long ptsTime = parseSpliceTime(sectionData, ptsAdjustment);\n    long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime);\n    return new TimeSignalCommand(ptsTime, playbackPositionUs);\n  }\n\n  /**\n   * Parses pts_time from splice_time(), defined in Section 9.4.1. Returns {@link C#TIME_UNSET}, if\n   * time_specified_flag is false.\n   *\n   * @param sectionData The section data from which the pts_time is parsed.\n   * @param ptsAdjustment The pts adjustment provided by the splice info section header.\n   * @return The pts_time defined by splice_time(), or {@link C#TIME_UNSET}, if time_specified_flag\n   *     is false.\n   */\n  /* package */ static long parseSpliceTime(ParsableByteArray sectionData, long ptsAdjustment) {\n    long firstByte = sectionData.readUnsignedByte();\n    long ptsTime = C.TIME_UNSET;\n    if ((firstByte & 0x80) != 0 /* time_specified_flag */) {\n      // See SCTE35 9.2.1 for more information about pts adjustment.\n      ptsTime = (firstByte & 0x01) << 32 | sectionData.readUnsignedInt();\n      ptsTime += ptsAdjustment;\n      ptsTime &= 0x1FFFFFFFFL;\n    }\n    return ptsTime;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeLong(ptsTime);\n    dest.writeLong(playbackPositionUs);\n  }\n\n  public static final Creator<TimeSignalCommand> CREATOR =\n      new Creator<TimeSignalCommand>() {\n\n    @Override\n    public TimeSignalCommand createFromParcel(Parcel in) {\n      return new TimeSignalCommand(in.readLong(), in.readLong());\n    }\n\n    @Override\n    public TimeSignalCommand[] newArray(int size) {\n      return new TimeSignalCommand[size];\n    }\n\n  };\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.metadata.scte35;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.offline.DownloadRequest.UnsupportedRequestException;\nimport com.google.android.exoplayer2.util.AtomicFile;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.DataInputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Loads {@link DownloadRequest DownloadRequests} from legacy action files.\n *\n * @deprecated Legacy action files should be merged into download indices using {@link\n *     ActionFileUpgradeUtil}.\n */\n@Deprecated\n/* package */ final class ActionFile {\n\n  private static final int VERSION = 0;\n\n  private final AtomicFile atomicFile;\n\n  /**\n   * @param actionFile The file from which {@link DownloadRequest DownloadRequests} will be loaded.\n   */\n  public ActionFile(File actionFile) {\n    atomicFile = new AtomicFile(actionFile);\n  }\n\n  /** Returns whether the file or its backup exists. */\n  public boolean exists() {\n    return atomicFile.exists();\n  }\n\n  /** Deletes the action file and its backup. */\n  public void delete() {\n    atomicFile.delete();\n  }\n\n  /**\n   * Loads {@link DownloadRequest DownloadRequests} from the file.\n   *\n   * @return The loaded {@link DownloadRequest DownloadRequests}, or an empty array if the file does\n   *     not exist.\n   * @throws IOException If there is an error reading the file.\n   */\n  public DownloadRequest[] load() throws IOException {\n    if (!exists()) {\n      return new DownloadRequest[0];\n    }\n    @Nullable InputStream inputStream = null;\n    try {\n      inputStream = atomicFile.openRead();\n      DataInputStream dataInputStream = new DataInputStream(inputStream);\n      int version = dataInputStream.readInt();\n      if (version > VERSION) {\n        throw new IOException(\"Unsupported action file version: \" + version);\n      }\n      int actionCount = dataInputStream.readInt();\n      ArrayList<DownloadRequest> actions = new ArrayList<>();\n      for (int i = 0; i < actionCount; i++) {\n        try {\n          actions.add(readDownloadRequest(dataInputStream));\n        } catch (UnsupportedRequestException e) {\n          // remove DownloadRequest is not supported. Ignore and continue loading rest.\n        }\n      }\n      return actions.toArray(new DownloadRequest[0]);\n    } finally {\n      Util.closeQuietly(inputStream);\n    }\n  }\n\n  private static DownloadRequest readDownloadRequest(DataInputStream input) throws IOException {\n    String type = input.readUTF();\n    int version = input.readInt();\n\n    Uri uri = Uri.parse(input.readUTF());\n    boolean isRemoveAction = input.readBoolean();\n\n    int dataLength = input.readInt();\n    @Nullable byte[] data;\n    if (dataLength != 0) {\n      data = new byte[dataLength];\n      input.readFully(data);\n    } else {\n      data = null;\n    }\n\n    // Serialized version 0 progressive actions did not contain keys.\n    boolean isLegacyProgressive = version == 0 && DownloadRequest.TYPE_PROGRESSIVE.equals(type);\n    List<StreamKey> keys = new ArrayList<>();\n    if (!isLegacyProgressive) {\n      int keyCount = input.readInt();\n      for (int i = 0; i < keyCount; i++) {\n        keys.add(readKey(type, version, input));\n      }\n    }\n\n    // Serialized version 0 and 1 DASH/HLS/SS actions did not contain a custom cache key.\n    boolean isLegacySegmented =\n        version < 2\n            && (DownloadRequest.TYPE_DASH.equals(type)\n                || DownloadRequest.TYPE_HLS.equals(type)\n                || DownloadRequest.TYPE_SS.equals(type));\n    @Nullable String customCacheKey = null;\n    if (!isLegacySegmented) {\n      customCacheKey = input.readBoolean() ? input.readUTF() : null;\n    }\n\n    // Serialized version 0, 1 and 2 did not contain an id. We need to generate one.\n    String id = version < 3 ? generateDownloadId(uri, customCacheKey) : input.readUTF();\n\n    if (isRemoveAction) {\n      // Remove actions are not supported anymore.\n      throw new UnsupportedRequestException();\n    }\n    return new DownloadRequest(id, type, uri, keys, customCacheKey, data);\n  }\n\n  private static StreamKey readKey(String type, int version, DataInputStream input)\n      throws IOException {\n    int periodIndex;\n    int groupIndex;\n    int trackIndex;\n\n    // Serialized version 0 HLS/SS actions did not contain a period index.\n    if ((DownloadRequest.TYPE_HLS.equals(type) || DownloadRequest.TYPE_SS.equals(type))\n        && version == 0) {\n      periodIndex = 0;\n      groupIndex = input.readInt();\n      trackIndex = input.readInt();\n    } else {\n      periodIndex = input.readInt();\n      groupIndex = input.readInt();\n      trackIndex = input.readInt();\n    }\n    return new StreamKey(periodIndex, groupIndex, trackIndex);\n  }\n\n  private static String generateDownloadId(Uri uri, @Nullable String customCacheKey) {\n    return customCacheKey != null ? customCacheKey : uri.toString();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport static com.google.android.exoplayer2.offline.Download.STATE_QUEUED;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.C;\nimport java.io.File;\nimport java.io.IOException;\n\n/** Utility class for upgrading legacy action files into {@link DefaultDownloadIndex}. */\npublic final class ActionFileUpgradeUtil {\n\n  /** Provides download IDs during action file upgrade. */\n  public interface DownloadIdProvider {\n\n    /**\n     * Returns a download id for given request.\n     *\n     * @param downloadRequest The request for which an ID is required.\n     * @return A corresponding download ID.\n     */\n    String getId(DownloadRequest downloadRequest);\n  }\n\n  private ActionFileUpgradeUtil() {}\n\n  /**\n   * Merges {@link DownloadRequest DownloadRequests} contained in a legacy action file into a {@link\n   * DefaultDownloadIndex}, deleting the action file if the merge is successful or if {@code\n   * deleteOnFailure} is {@code true}.\n   *\n   * <p>This method must not be called while the {@link DefaultDownloadIndex} is being used by a\n   * {@link DownloadManager}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param actionFilePath The action file path.\n   * @param downloadIdProvider A download ID provider, or {@code null}. If {@code null} then ID of\n   *     each download will be its custom cache key if one is specified, or else its URL.\n   * @param downloadIndex The index into which the requests will be merged.\n   * @param deleteOnFailure Whether to delete the action file if the merge fails.\n   * @param addNewDownloadsAsCompleted Whether to add new downloads as completed.\n   * @throws IOException If an error occurs loading or merging the requests.\n   */\n  @WorkerThread\n  @SuppressWarnings(\"deprecation\")\n  public static void upgradeAndDelete(\n      File actionFilePath,\n      @Nullable DownloadIdProvider downloadIdProvider,\n      DefaultDownloadIndex downloadIndex,\n      boolean deleteOnFailure,\n      boolean addNewDownloadsAsCompleted)\n      throws IOException {\n    ActionFile actionFile = new ActionFile(actionFilePath);\n    if (actionFile.exists()) {\n      boolean success = false;\n      try {\n        long nowMs = System.currentTimeMillis();\n        for (DownloadRequest request : actionFile.load()) {\n          if (downloadIdProvider != null) {\n            request = request.copyWithId(downloadIdProvider.getId(request));\n          }\n          mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted, nowMs);\n        }\n        success = true;\n      } finally {\n        if (success || deleteOnFailure) {\n          actionFile.delete();\n        }\n      }\n    }\n  }\n\n  /**\n   * Merges a {@link DownloadRequest} into a {@link DefaultDownloadIndex}.\n   *\n   * @param request The request to be merged.\n   * @param downloadIndex The index into which the request will be merged.\n   * @param addNewDownloadAsCompleted Whether to add new downloads as completed.\n   * @throws IOException If an error occurs merging the request.\n   */\n  /* package */ static void mergeRequest(\n      DownloadRequest request,\n      DefaultDownloadIndex downloadIndex,\n      boolean addNewDownloadAsCompleted,\n      long nowMs)\n      throws IOException {\n    @Nullable Download download = downloadIndex.getDownload(request.id);\n    if (download != null) {\n      download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs);\n    } else {\n      download =\n          new Download(\n              request,\n              addNewDownloadAsCompleted ? Download.STATE_COMPLETED : STATE_QUEUED,\n              /* startTimeMs= */ nowMs,\n              /* updateTimeMs= */ nowMs,\n              /* contentLength= */ C.LENGTH_UNSET,\n              Download.STOP_REASON_NONE,\n              Download.FAILURE_REASON_NONE);\n    }\n    downloadIndex.putDownload(download);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.database.DatabaseIOException;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.database.VersionTable;\nimport com.google.android.exoplayer2.offline.Download.FailureReason;\nimport com.google.android.exoplayer2.offline.Download.State;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** A {@link DownloadIndex} that uses SQLite to persist {@link Download Downloads}. */\npublic final class DefaultDownloadIndex implements WritableDownloadIndex {\n\n  private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + \"Downloads\";\n\n  @VisibleForTesting /* package */ static final int TABLE_VERSION = 2;\n\n  private static final String COLUMN_ID = \"id\";\n  private static final String COLUMN_TYPE = \"title\";\n  private static final String COLUMN_URI = \"uri\";\n  private static final String COLUMN_STREAM_KEYS = \"stream_keys\";\n  private static final String COLUMN_CUSTOM_CACHE_KEY = \"custom_cache_key\";\n  private static final String COLUMN_DATA = \"data\";\n  private static final String COLUMN_STATE = \"state\";\n  private static final String COLUMN_START_TIME_MS = \"start_time_ms\";\n  private static final String COLUMN_UPDATE_TIME_MS = \"update_time_ms\";\n  private static final String COLUMN_CONTENT_LENGTH = \"content_length\";\n  private static final String COLUMN_STOP_REASON = \"stop_reason\";\n  private static final String COLUMN_FAILURE_REASON = \"failure_reason\";\n  private static final String COLUMN_PERCENT_DOWNLOADED = \"percent_downloaded\";\n  private static final String COLUMN_BYTES_DOWNLOADED = \"bytes_downloaded\";\n\n  private static final int COLUMN_INDEX_ID = 0;\n  private static final int COLUMN_INDEX_TYPE = 1;\n  private static final int COLUMN_INDEX_URI = 2;\n  private static final int COLUMN_INDEX_STREAM_KEYS = 3;\n  private static final int COLUMN_INDEX_CUSTOM_CACHE_KEY = 4;\n  private static final int COLUMN_INDEX_DATA = 5;\n  private static final int COLUMN_INDEX_STATE = 6;\n  private static final int COLUMN_INDEX_START_TIME_MS = 7;\n  private static final int COLUMN_INDEX_UPDATE_TIME_MS = 8;\n  private static final int COLUMN_INDEX_CONTENT_LENGTH = 9;\n  private static final int COLUMN_INDEX_STOP_REASON = 10;\n  private static final int COLUMN_INDEX_FAILURE_REASON = 11;\n  private static final int COLUMN_INDEX_PERCENT_DOWNLOADED = 12;\n  private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13;\n\n  private static final String WHERE_ID_EQUALS = COLUMN_ID + \" = ?\";\n  private static final String WHERE_STATE_IS_DOWNLOADING =\n      COLUMN_STATE + \" = \" + Download.STATE_DOWNLOADING;\n  private static final String WHERE_STATE_IS_TERMINAL =\n      getStateQuery(Download.STATE_COMPLETED, Download.STATE_FAILED);\n\n  private static final String[] COLUMNS =\n      new String[] {\n        COLUMN_ID,\n        COLUMN_TYPE,\n        COLUMN_URI,\n        COLUMN_STREAM_KEYS,\n        COLUMN_CUSTOM_CACHE_KEY,\n        COLUMN_DATA,\n        COLUMN_STATE,\n        COLUMN_START_TIME_MS,\n        COLUMN_UPDATE_TIME_MS,\n        COLUMN_CONTENT_LENGTH,\n        COLUMN_STOP_REASON,\n        COLUMN_FAILURE_REASON,\n        COLUMN_PERCENT_DOWNLOADED,\n        COLUMN_BYTES_DOWNLOADED,\n      };\n\n  private static final String TABLE_SCHEMA =\n      \"(\"\n          + COLUMN_ID\n          + \" TEXT PRIMARY KEY NOT NULL,\"\n          + COLUMN_TYPE\n          + \" TEXT NOT NULL,\"\n          + COLUMN_URI\n          + \" TEXT NOT NULL,\"\n          + COLUMN_STREAM_KEYS\n          + \" TEXT NOT NULL,\"\n          + COLUMN_CUSTOM_CACHE_KEY\n          + \" TEXT,\"\n          + COLUMN_DATA\n          + \" BLOB NOT NULL,\"\n          + COLUMN_STATE\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_START_TIME_MS\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_UPDATE_TIME_MS\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_CONTENT_LENGTH\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_STOP_REASON\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_FAILURE_REASON\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_PERCENT_DOWNLOADED\n          + \" REAL NOT NULL,\"\n          + COLUMN_BYTES_DOWNLOADED\n          + \" INTEGER NOT NULL)\";\n\n  private static final String TRUE = \"1\";\n\n  private final String name;\n  private final String tableName;\n  private final DatabaseProvider databaseProvider;\n\n  private boolean initialized;\n\n  /**\n   * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided\n   * by a {@link DatabaseProvider}.\n   *\n   * <p>Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code\n   * name=\"\"}.\n   *\n   * <p>Applications that only have one download index may use this constructor. Applications that\n   * have multiple download indices should call {@link #DefaultDownloadIndex(DatabaseProvider,\n   * String)} to specify a unique name for each index.\n   *\n   * @param databaseProvider Provides the SQLite database in which downloads are persisted.\n   */\n  public DefaultDownloadIndex(DatabaseProvider databaseProvider) {\n    this(databaseProvider, \"\");\n  }\n\n  /**\n   * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided\n   * by a {@link DatabaseProvider}.\n   *\n   * @param databaseProvider Provides the SQLite database in which downloads are persisted.\n   * @param name The name of the index. This name is incorporated into the names of the SQLite\n   *     tables in which downloads are persisted.\n   */\n  public DefaultDownloadIndex(DatabaseProvider databaseProvider, String name) {\n    this.name = name;\n    this.databaseProvider = databaseProvider;\n    tableName = TABLE_PREFIX + name;\n  }\n\n  @Override\n  @Nullable\n  public Download getDownload(String id) throws DatabaseIOException {\n    ensureInitialized();\n    try (Cursor cursor = getCursor(WHERE_ID_EQUALS, new String[] {id})) {\n      if (cursor.getCount() == 0) {\n        return null;\n      }\n      cursor.moveToNext();\n      return getDownloadForCurrentRow(cursor);\n    } catch (SQLiteException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public DownloadCursor getDownloads(@State int... states) throws DatabaseIOException {\n    ensureInitialized();\n    Cursor cursor = getCursor(getStateQuery(states), /* selectionArgs= */ null);\n    return new DownloadCursorImpl(cursor);\n  }\n\n  @Override\n  public void putDownload(Download download) throws DatabaseIOException {\n    ensureInitialized();\n    ContentValues values = new ContentValues();\n    values.put(COLUMN_ID, download.request.id);\n    values.put(COLUMN_TYPE, download.request.type);\n    values.put(COLUMN_URI, download.request.uri.toString());\n    values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(download.request.streamKeys));\n    values.put(COLUMN_CUSTOM_CACHE_KEY, download.request.customCacheKey);\n    values.put(COLUMN_DATA, download.request.data);\n    values.put(COLUMN_STATE, download.state);\n    values.put(COLUMN_START_TIME_MS, download.startTimeMs);\n    values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs);\n    values.put(COLUMN_CONTENT_LENGTH, download.contentLength);\n    values.put(COLUMN_STOP_REASON, download.stopReason);\n    values.put(COLUMN_FAILURE_REASON, download.failureReason);\n    values.put(COLUMN_PERCENT_DOWNLOADED, download.getPercentDownloaded());\n    values.put(COLUMN_BYTES_DOWNLOADED, download.getBytesDownloaded());\n    try {\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);\n    } catch (SQLiteException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public void removeDownload(String id) throws DatabaseIOException {\n    ensureInitialized();\n    try {\n      databaseProvider.getWritableDatabase().delete(tableName, WHERE_ID_EQUALS, new String[] {id});\n    } catch (SQLiteException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public void setDownloadingStatesToQueued() throws DatabaseIOException {\n    ensureInitialized();\n    try {\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_STATE, Download.STATE_QUEUED);\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.update(tableName, values, WHERE_STATE_IS_DOWNLOADING, /* whereArgs= */ null);\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public void setStatesToRemoving() throws DatabaseIOException {\n    ensureInitialized();\n    try {\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_STATE, Download.STATE_REMOVING);\n      // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in\n      // case we're moving downloads from STATE_FAILED to STATE_REMOVING.\n      values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE);\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null);\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public void setStopReason(int stopReason) throws DatabaseIOException {\n    ensureInitialized();\n    try {\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_STOP_REASON, stopReason);\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.update(tableName, values, WHERE_STATE_IS_TERMINAL, /* whereArgs= */ null);\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  @Override\n  public void setStopReason(String id, int stopReason) throws DatabaseIOException {\n    ensureInitialized();\n    try {\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_STOP_REASON, stopReason);\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.update(\n          tableName,\n          values,\n          WHERE_STATE_IS_TERMINAL + \" AND \" + WHERE_ID_EQUALS,\n          new String[] {id});\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  private void ensureInitialized() throws DatabaseIOException {\n    if (initialized) {\n      return;\n    }\n    try {\n      SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();\n      int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name);\n      if (version != TABLE_VERSION) {\n        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n        writableDatabase.beginTransactionNonExclusive();\n        try {\n          VersionTable.setVersion(\n              writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION);\n          writableDatabase.execSQL(\"DROP TABLE IF EXISTS \" + tableName);\n          writableDatabase.execSQL(\"CREATE TABLE \" + tableName + \" \" + TABLE_SCHEMA);\n          writableDatabase.setTransactionSuccessful();\n        } finally {\n          writableDatabase.endTransaction();\n        }\n      }\n      initialized = true;\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  // incompatible types in argument.\n  @SuppressWarnings(\"nullness:argument.type.incompatible\")\n  private Cursor getCursor(String selection, @Nullable String[] selectionArgs)\n      throws DatabaseIOException {\n    try {\n      String sortOrder = COLUMN_START_TIME_MS + \" ASC\";\n      return databaseProvider\n          .getReadableDatabase()\n          .query(\n              tableName,\n              COLUMNS,\n              selection,\n              selectionArgs,\n              /* groupBy= */ null,\n              /* having= */ null,\n              sortOrder);\n    } catch (SQLiteException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  private static String getStateQuery(@State int... states) {\n    if (states.length == 0) {\n      return TRUE;\n    }\n    StringBuilder selectionBuilder = new StringBuilder();\n    selectionBuilder.append(COLUMN_STATE).append(\" IN (\");\n    for (int i = 0; i < states.length; i++) {\n      if (i > 0) {\n        selectionBuilder.append(',');\n      }\n      selectionBuilder.append(states[i]);\n    }\n    selectionBuilder.append(')');\n    return selectionBuilder.toString();\n  }\n\n  private static Download getDownloadForCurrentRow(Cursor cursor) {\n    DownloadRequest request =\n        new DownloadRequest(\n            /* id= */ cursor.getString(COLUMN_INDEX_ID),\n            /* type= */ cursor.getString(COLUMN_INDEX_TYPE),\n            /* uri= */ Uri.parse(cursor.getString(COLUMN_INDEX_URI)),\n            /* streamKeys= */ decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),\n            /* customCacheKey= */ cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY),\n            /* data= */ cursor.getBlob(COLUMN_INDEX_DATA));\n    DownloadProgress downloadProgress = new DownloadProgress();\n    downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED);\n    downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED);\n    @State int state = cursor.getInt(COLUMN_INDEX_STATE);\n    // It's possible the database contains failure reasons for non-failed downloads, which is\n    // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785.\n    @FailureReason\n    int failureReason =\n        state == Download.STATE_FAILED\n            ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON)\n            : Download.FAILURE_REASON_NONE;\n    return new Download(\n        request,\n        state,\n        /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS),\n        /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),\n        /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH),\n        /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON),\n        failureReason,\n        downloadProgress);\n  }\n\n  private static String encodeStreamKeys(List<StreamKey> streamKeys) {\n    StringBuilder stringBuilder = new StringBuilder();\n    for (int i = 0; i < streamKeys.size(); i++) {\n      StreamKey streamKey = streamKeys.get(i);\n      stringBuilder\n          .append(streamKey.periodIndex)\n          .append('.')\n          .append(streamKey.groupIndex)\n          .append('.')\n          .append(streamKey.trackIndex)\n          .append(',');\n    }\n    if (stringBuilder.length() > 0) {\n      stringBuilder.setLength(stringBuilder.length() - 1);\n    }\n    return stringBuilder.toString();\n  }\n\n  private static List<StreamKey> decodeStreamKeys(String encodedStreamKeys) {\n    ArrayList<StreamKey> streamKeys = new ArrayList<>();\n    if (encodedStreamKeys.isEmpty()) {\n      return streamKeys;\n    }\n    String[] streamKeysStrings = Util.split(encodedStreamKeys, \",\");\n    for (String streamKeysString : streamKeysStrings) {\n      String[] indices = Util.split(streamKeysString, \"\\\\.\");\n      Assertions.checkState(indices.length == 3);\n      streamKeys.add(\n          new StreamKey(\n              Integer.parseInt(indices[0]),\n              Integer.parseInt(indices[1]),\n              Integer.parseInt(indices[2])));\n    }\n    return streamKeys;\n  }\n\n  private static final class DownloadCursorImpl implements DownloadCursor {\n\n    private final Cursor cursor;\n\n    private DownloadCursorImpl(Cursor cursor) {\n      this.cursor = cursor;\n    }\n\n    @Override\n    public Download getDownload() {\n      return getDownloadForCurrentRow(cursor);\n    }\n\n    @Override\n    public int getCount() {\n      return cursor.getCount();\n    }\n\n    @Override\n    public int getPosition() {\n      return cursor.getPosition();\n    }\n\n    @Override\n    public boolean moveToPosition(int position) {\n      return cursor.moveToPosition(position);\n    }\n\n    @Override\n    public void close() {\n      cursor.close();\n    }\n\n    @Override\n    public boolean isClosed() {\n      return cursor.isClosed();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport java.lang.reflect.Constructor;\nimport java.util.List;\n\n/**\n * Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and\n * SmoothStreaming downloaders. Note that for the latter three, the corresponding library module\n * must be built into the application.\n */\npublic class DefaultDownloaderFactory implements DownloaderFactory {\n\n  @Nullable private static final Constructor<? extends Downloader> DASH_DOWNLOADER_CONSTRUCTOR;\n  @Nullable private static final Constructor<? extends Downloader> HLS_DOWNLOADER_CONSTRUCTOR;\n  @Nullable private static final Constructor<? extends Downloader> SS_DOWNLOADER_CONSTRUCTOR;\n\n  static {\n    Constructor<? extends Downloader> dashDownloaderConstructor = null;\n    try {\n      // LINT.IfChange\n      dashDownloaderConstructor =\n          getDownloaderConstructor(\n              Class.forName(\"com.google.android.exoplayer2.source.dash.offline.DashDownloader\"));\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the DASH module.\n    }\n    DASH_DOWNLOADER_CONSTRUCTOR = dashDownloaderConstructor;\n    Constructor<? extends Downloader> hlsDownloaderConstructor = null;\n    try {\n      // LINT.IfChange\n      hlsDownloaderConstructor =\n          getDownloaderConstructor(\n              Class.forName(\"com.google.android.exoplayer2.source.hls.offline.HlsDownloader\"));\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the HLS module.\n    }\n    HLS_DOWNLOADER_CONSTRUCTOR = hlsDownloaderConstructor;\n    Constructor<? extends Downloader> ssDownloaderConstructor = null;\n    try {\n      // LINT.IfChange\n      ssDownloaderConstructor =\n          getDownloaderConstructor(\n              Class.forName(\n                  \"com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader\"));\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the SmoothStreaming module.\n    }\n    SS_DOWNLOADER_CONSTRUCTOR = ssDownloaderConstructor;\n  }\n\n  private final DownloaderConstructorHelper downloaderConstructorHelper;\n\n  /** @param downloaderConstructorHelper A helper for instantiating downloaders. */\n  public DefaultDownloaderFactory(DownloaderConstructorHelper downloaderConstructorHelper) {\n    this.downloaderConstructorHelper = downloaderConstructorHelper;\n  }\n\n  @Override\n  public Downloader createDownloader(DownloadRequest request) {\n    switch (request.type) {\n      case DownloadRequest.TYPE_PROGRESSIVE:\n        return new ProgressiveDownloader(\n            request.uri, request.customCacheKey, downloaderConstructorHelper);\n      case DownloadRequest.TYPE_DASH:\n        return createDownloader(request, DASH_DOWNLOADER_CONSTRUCTOR);\n      case DownloadRequest.TYPE_HLS:\n        return createDownloader(request, HLS_DOWNLOADER_CONSTRUCTOR);\n      case DownloadRequest.TYPE_SS:\n        return createDownloader(request, SS_DOWNLOADER_CONSTRUCTOR);\n      default:\n        throw new IllegalArgumentException(\"Unsupported type: \" + request.type);\n    }\n  }\n\n  private Downloader createDownloader(\n      DownloadRequest request, @Nullable Constructor<? extends Downloader> constructor) {\n    if (constructor == null) {\n      throw new IllegalStateException(\"Module missing for: \" + request.type);\n    }\n    try {\n      return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper);\n    } catch (Exception e) {\n      throw new RuntimeException(\"Failed to instantiate downloader for: \" + request.type, e);\n    }\n  }\n\n  // LINT.IfChange\n  private static Constructor<? extends Downloader> getDownloaderConstructor(Class<?> clazz) {\n    try {\n      return clazz\n          .asSubclass(Downloader.class)\n          .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class);\n    } catch (NoSuchMethodException e) {\n      // The downloader is present, but the expected constructor is missing.\n      throw new RuntimeException(\"Downloader constructor missing\", e);\n    }\n  }\n  // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/Download.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Represents state of a download. */\npublic final class Download {\n\n  /**\n   * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STOPPED}, {@link\n   * #STATE_DOWNLOADING}, {@link #STATE_COMPLETED}, {@link #STATE_FAILED}, {@link #STATE_REMOVING}\n   * or {@link #STATE_RESTARTING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    STATE_QUEUED,\n    STATE_STOPPED,\n    STATE_DOWNLOADING,\n    STATE_COMPLETED,\n    STATE_FAILED,\n    STATE_REMOVING,\n    STATE_RESTARTING\n  })\n  public @interface State {}\n  // Important: These constants are persisted into DownloadIndex. Do not change them.\n  /**\n   * The download is waiting to be started. A download may be queued because the {@link\n   * DownloadManager}\n   *\n   * <ul>\n   *   <li>Is {@link DownloadManager#getDownloadsPaused() paused}\n   *   <li>Has {@link DownloadManager#getRequirements() Requirements} that are not met\n   *   <li>Has already started {@link DownloadManager#getMaxParallelDownloads()\n   *       maxParallelDownloads}\n   * </ul>\n   */\n  public static final int STATE_QUEUED = 0;\n  /** The download is stopped for a specified {@link #stopReason}. */\n  public static final int STATE_STOPPED = 1;\n  /** The download is currently started. */\n  public static final int STATE_DOWNLOADING = 2;\n  /** The download completed. */\n  public static final int STATE_COMPLETED = 3;\n  /** The download failed. */\n  public static final int STATE_FAILED = 4;\n  /** The download is being removed. */\n  public static final int STATE_REMOVING = 5;\n  /** The download will restart after all downloaded data is removed. */\n  public static final int STATE_RESTARTING = 7;\n\n  /** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({FAILURE_REASON_NONE, FAILURE_REASON_UNKNOWN})\n  public @interface FailureReason {}\n  /** The download isn't failed. */\n  public static final int FAILURE_REASON_NONE = 0;\n  /** The download is failed because of unknown reason. */\n  public static final int FAILURE_REASON_UNKNOWN = 1;\n\n  /** The download isn't stopped. */\n  public static final int STOP_REASON_NONE = 0;\n\n  /** The download request. */\n  public final DownloadRequest request;\n  /** The state of the download. */\n  @State public final int state;\n  /** The first time when download entry is created. */\n  public final long startTimeMs;\n  /** The last update time. */\n  public final long updateTimeMs;\n  /** The total size of the content in bytes, or {@link C#LENGTH_UNSET} if unknown. */\n  public final long contentLength;\n  /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */\n  public final int stopReason;\n  /**\n   * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link\n   * #FAILURE_REASON_NONE}.\n   */\n  @FailureReason public final int failureReason;\n\n  /* package */ final DownloadProgress progress;\n\n  public Download(\n      DownloadRequest request,\n      @State int state,\n      long startTimeMs,\n      long updateTimeMs,\n      long contentLength,\n      int stopReason,\n      @FailureReason int failureReason) {\n    this(\n        request,\n        state,\n        startTimeMs,\n        updateTimeMs,\n        contentLength,\n        stopReason,\n        failureReason,\n        new DownloadProgress());\n  }\n\n  public Download(\n      DownloadRequest request,\n      @State int state,\n      long startTimeMs,\n      long updateTimeMs,\n      long contentLength,\n      int stopReason,\n      @FailureReason int failureReason,\n      DownloadProgress progress) {\n    Assertions.checkNotNull(progress);\n    Assertions.checkArgument((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED));\n    if (stopReason != 0) {\n      Assertions.checkArgument(state != STATE_DOWNLOADING && state != STATE_QUEUED);\n    }\n    this.request = request;\n    this.state = state;\n    this.startTimeMs = startTimeMs;\n    this.updateTimeMs = updateTimeMs;\n    this.contentLength = contentLength;\n    this.stopReason = stopReason;\n    this.failureReason = failureReason;\n    this.progress = progress;\n  }\n\n  /** Returns whether the download is completed or failed. These are terminal states. */\n  public boolean isTerminalState() {\n    return state == STATE_COMPLETED || state == STATE_FAILED;\n  }\n\n  /** Returns the total number of downloaded bytes. */\n  public long getBytesDownloaded() {\n    return progress.bytesDownloaded;\n  }\n\n  /**\n   * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is\n   * available.\n   */\n  public float getPercentDownloaded() {\n    return progress.percentDownloaded;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadCursor.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport java.io.Closeable;\n\n/** Provides random read-write access to the result set returned by a database query. */\npublic interface DownloadCursor extends Closeable {\n\n  /** Returns the download at the current position. */\n  Download getDownload();\n\n  /** Returns the numbers of downloads in the cursor. */\n  int getCount();\n\n  /**\n   * Returns the current position of the cursor in the download set. The value is zero-based. When\n   * the download set is first returned the cursor will be at positon -1, which is before the first\n   * download. After the last download is returned another call to next() will leave the cursor past\n   * the last entry, at a position of count().\n   *\n   * @return the current cursor position.\n   */\n  int getPosition();\n\n  /**\n   * Move the cursor to an absolute position. The valid range of values is -1 &lt;= position &lt;=\n   * count.\n   *\n   * <p>This method will return true if the request destination was reachable, otherwise, it returns\n   * false.\n   *\n   * @param position the zero-based position to move to.\n   * @return whether the requested move fully succeeded.\n   */\n  boolean moveToPosition(int position);\n\n  /**\n   * Move the cursor to the first download.\n   *\n   * <p>This method will return false if the cursor is empty.\n   *\n   * @return whether the move succeeded.\n   */\n  default boolean moveToFirst() {\n    return moveToPosition(0);\n  }\n\n  /**\n   * Move the cursor to the last download.\n   *\n   * <p>This method will return false if the cursor is empty.\n   *\n   * @return whether the move succeeded.\n   */\n  default boolean moveToLast() {\n    return moveToPosition(getCount() - 1);\n  }\n\n  /**\n   * Move the cursor to the next download.\n   *\n   * <p>This method will return false if the cursor is already past the last entry in the result\n   * set.\n   *\n   * @return whether the move succeeded.\n   */\n  default boolean moveToNext() {\n    return moveToPosition(getPosition() + 1);\n  }\n\n  /**\n   * Move the cursor to the previous download.\n   *\n   * <p>This method will return false if the cursor is already before the first entry in the result\n   * set.\n   *\n   * @return whether the move succeeded.\n   */\n  default boolean moveToPrevious() {\n    return moveToPosition(getPosition() - 1);\n  }\n\n  /** Returns whether the cursor is pointing to the first download. */\n  default boolean isFirst() {\n    return getPosition() == 0 && getCount() != 0;\n  }\n\n  /** Returns whether the cursor is pointing to the last download. */\n  default boolean isLast() {\n    int count = getCount();\n    return getPosition() == (count - 1) && count != 0;\n  }\n\n  /** Returns whether the cursor is pointing to the position before the first download. */\n  default boolean isBeforeFirst() {\n    if (getCount() == 0) {\n      return true;\n    }\n    return getPosition() == -1;\n  }\n\n  /** Returns whether the cursor is pointing to the position after the last download. */\n  default boolean isAfterLast() {\n    if (getCount() == 0) {\n      return true;\n    }\n    return getPosition() == getCount();\n  }\n\n  /** Returns whether the cursor is closed */\n  boolean isClosed();\n\n  @Override\n  void close();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport java.io.IOException;\n\n/** Thrown on an error during downloading. */\npublic final class DownloadException extends IOException {\n\n  /** @param message The message for the exception. */\n  public DownloadException(String message) {\n    super(message);\n  }\n\n  /** @param cause The cause for the exception. */\n  public DownloadException(Throwable cause) {\n    super(cause);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport android.util.SparseIntArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;\nimport com.google.android.exoplayer2.source.MediaSourceFactory;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.trackselection.BaseTrackSelection;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectorResult;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\nimport com.google.android.exoplayer2.upstream.DefaultAllocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.reflect.Constructor;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * A helper for initializing and removing downloads.\n *\n * <p>The helper extracts track information from the media, selects tracks for downloading, and\n * creates {@link DownloadRequest download requests} based on the selected tracks.\n *\n * <p>A typical usage of DownloadHelper follows these steps:\n *\n * <ol>\n *   <li>Build the helper using one of the {@code forXXX} methods.\n *   <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.\n *   <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link\n *       #getTrackSelections(int, int)}, and make adjustments using {@link\n *       #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link\n *       #addTrackSelection(int, Parameters)}.\n *   <li>Create a download request for the selected track using {@link #getDownloadRequest(byte[])}.\n *   <li>Release the helper using {@link #release()}.\n * </ol>\n */\npublic final class DownloadHelper {\n\n  /**\n   * Default track selection parameters for downloading, but without any {@link Context}\n   * constraints.\n   *\n   * <p>If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead.\n   *\n   * @see Parameters#DEFAULT_WITHOUT_CONTEXT\n   */\n  public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT =\n      Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build();\n\n  /**\n   * @deprecated This instance does not have {@link Context} constraints. Use {@link\n   *     #getDefaultTrackSelectorParameters(Context)} instead.\n   */\n  @Deprecated\n  public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT =\n      DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT;\n\n  /**\n   * @deprecated This instance does not have {@link Context} constraints. Use {@link\n   *     #getDefaultTrackSelectorParameters(Context)} instead.\n   */\n  @Deprecated\n  public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS =\n      DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT;\n\n  /** Returns the default parameters used for track selection for downloading. */\n  public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) {\n    return Parameters.getDefaults(context)\n        .buildUpon()\n        .setForceHighestSupportedBitrate(true)\n        .build();\n  }\n\n  /** A callback to be notified when the {@link DownloadHelper} is prepared. */\n  public interface Callback {\n\n    /**\n     * Called when preparation completes.\n     *\n     * @param helper The reporting {@link DownloadHelper}.\n     */\n    void onPrepared(DownloadHelper helper);\n\n    /**\n     * Called when preparation fails.\n     *\n     * @param helper The reporting {@link DownloadHelper}.\n     * @param e The error.\n     */\n    void onPrepareError(DownloadHelper helper, IOException e);\n  }\n\n  /** Thrown at an attempt to download live content. */\n  public static class LiveContentUnsupportedException extends IOException {}\n\n  @Nullable\n  private static final Constructor<? extends MediaSourceFactory> DASH_FACTORY_CONSTRUCTOR =\n      getConstructor(\"com.google.android.exoplayer2.source.dash.DashMediaSource$Factory\");\n\n  @Nullable\n  private static final Constructor<? extends MediaSourceFactory> SS_FACTORY_CONSTRUCTOR =\n      getConstructor(\"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory\");\n\n  @Nullable\n  private static final Constructor<? extends MediaSourceFactory> HLS_FACTORY_CONSTRUCTOR =\n      getConstructor(\"com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory\");\n\n  /** @deprecated Use {@link #forProgressive(Context, Uri)} */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static DownloadHelper forProgressive(Uri uri) {\n    return forProgressive(uri, /* cacheKey= */ null);\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for progressive streams.\n   *\n   * @param context Any {@link Context}.\n   * @param uri A stream {@link Uri}.\n   * @return A {@link DownloadHelper} for progressive streams.\n   */\n  public static DownloadHelper forProgressive(Context context, Uri uri) {\n    return forProgressive(context, uri, /* cacheKey= */ null);\n  }\n\n  /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */\n  @Deprecated\n  public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) {\n    return new DownloadHelper(\n        DownloadRequest.TYPE_PROGRESSIVE,\n        uri,\n        cacheKey,\n        /* mediaSource= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT,\n        /* rendererCapabilities= */ new RendererCapabilities[0]);\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for progressive streams.\n   *\n   * @param context Any {@link Context}.\n   * @param uri A stream {@link Uri}.\n   * @param cacheKey An optional cache key.\n   * @return A {@link DownloadHelper} for progressive streams.\n   */\n  public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) {\n    return new DownloadHelper(\n        DownloadRequest.TYPE_PROGRESSIVE,\n        uri,\n        cacheKey,\n        /* mediaSource= */ null,\n        getDefaultTrackSelectorParameters(context),\n        /* rendererCapabilities= */ new RendererCapabilities[0]);\n  }\n\n  /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */\n  @Deprecated\n  public static DownloadHelper forDash(\n      Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forDash(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT);\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for DASH streams.\n   *\n   * @param context Any {@link Context}.\n   * @param uri A manifest {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @return A {@link DownloadHelper} for DASH streams.\n   * @throws IllegalStateException If the DASH module is missing.\n   */\n  public static DownloadHelper forDash(\n      Context context,\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory) {\n    return forDash(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        getDefaultTrackSelectorParameters(context));\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for DASH streams.\n   *\n   * @param uri A manifest {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by\n   *     {@code renderersFactory}.\n   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for\n   *     downloading.\n   * @return A {@link DownloadHelper} for DASH streams.\n   * @throws IllegalStateException If the DASH module is missing.\n   */\n  public static DownloadHelper forDash(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      DefaultTrackSelector.Parameters trackSelectorParameters) {\n    return new DownloadHelper(\n        DownloadRequest.TYPE_DASH,\n        uri,\n        /* cacheKey= */ null,\n        createMediaSourceInternal(\n            DASH_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null),\n        trackSelectorParameters,\n        Util.getRendererCapabilities(renderersFactory, drmSessionManager));\n  }\n\n  /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */\n  @Deprecated\n  public static DownloadHelper forHls(\n      Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forHls(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT);\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for HLS streams.\n   *\n   * @param context Any {@link Context}.\n   * @param uri A playlist {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @return A {@link DownloadHelper} for HLS streams.\n   * @throws IllegalStateException If the HLS module is missing.\n   */\n  public static DownloadHelper forHls(\n      Context context,\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory) {\n    return forHls(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        getDefaultTrackSelectorParameters(context));\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for HLS streams.\n   *\n   * @param uri A playlist {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by\n   *     {@code renderersFactory}.\n   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for\n   *     downloading.\n   * @return A {@link DownloadHelper} for HLS streams.\n   * @throws IllegalStateException If the HLS module is missing.\n   */\n  public static DownloadHelper forHls(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      DefaultTrackSelector.Parameters trackSelectorParameters) {\n    return new DownloadHelper(\n        DownloadRequest.TYPE_HLS,\n        uri,\n        /* cacheKey= */ null,\n        createMediaSourceInternal(\n            HLS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null),\n        trackSelectorParameters,\n        Util.getRendererCapabilities(renderersFactory, drmSessionManager));\n  }\n\n  /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */\n  @Deprecated\n  public static DownloadHelper forSmoothStreaming(\n      Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forSmoothStreaming(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT);\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for SmoothStreaming streams.\n   *\n   * @param context Any {@link Context}.\n   * @param uri A manifest {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @return A {@link DownloadHelper} for SmoothStreaming streams.\n   * @throws IllegalStateException If the SmoothStreaming module is missing.\n   */\n  public static DownloadHelper forSmoothStreaming(\n      Context context,\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory) {\n    return forSmoothStreaming(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        getDefaultTrackSelectorParameters(context));\n  }\n\n  /**\n   * Creates a {@link DownloadHelper} for SmoothStreaming streams.\n   *\n   * @param uri A manifest {@link Uri}.\n   * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.\n   * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are\n   *     selected.\n   * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by\n   *     {@code renderersFactory}.\n   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for\n   *     downloading.\n   * @return A {@link DownloadHelper} for SmoothStreaming streams.\n   * @throws IllegalStateException If the SmoothStreaming module is missing.\n   */\n  public static DownloadHelper forSmoothStreaming(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      RenderersFactory renderersFactory,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      DefaultTrackSelector.Parameters trackSelectorParameters) {\n    return new DownloadHelper(\n        DownloadRequest.TYPE_SS,\n        uri,\n        /* cacheKey= */ null,\n        createMediaSourceInternal(\n            SS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null),\n        trackSelectorParameters,\n        Util.getRendererCapabilities(renderersFactory, drmSessionManager));\n  }\n\n  /**\n   * Utility method to create a MediaSource which only contains the tracks defined in {@code\n   * downloadRequest}.\n   *\n   * @param downloadRequest A {@link DownloadRequest}.\n   * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n   * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}.\n   */\n  public static MediaSource createMediaSource(\n      DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) {\n    @Nullable Constructor<? extends MediaSourceFactory> constructor;\n    switch (downloadRequest.type) {\n      case DownloadRequest.TYPE_DASH:\n        constructor = DASH_FACTORY_CONSTRUCTOR;\n        break;\n      case DownloadRequest.TYPE_SS:\n        constructor = SS_FACTORY_CONSTRUCTOR;\n        break;\n      case DownloadRequest.TYPE_HLS:\n        constructor = HLS_FACTORY_CONSTRUCTOR;\n        break;\n      case DownloadRequest.TYPE_PROGRESSIVE:\n        return new ProgressiveMediaSource.Factory(dataSourceFactory)\n            .createMediaSource(downloadRequest.uri);\n      default:\n        throw new IllegalStateException(\"Unsupported type: \" + downloadRequest.type);\n    }\n    return createMediaSourceInternal(\n        constructor, downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys);\n  }\n\n  private final String downloadType;\n  private final Uri uri;\n  @Nullable private final String cacheKey;\n  @Nullable private final MediaSource mediaSource;\n  private final DefaultTrackSelector trackSelector;\n  private final RendererCapabilities[] rendererCapabilities;\n  private final SparseIntArray scratchSet;\n  private final Handler callbackHandler;\n  private final Timeline.Window window;\n\n  private boolean isPreparedWithMedia;\n  private @MonotonicNonNull Callback callback;\n  private @MonotonicNonNull MediaPreparer mediaPreparer;\n  private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;\n  private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;\n  private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;\n  private List<TrackSelection> @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer;\n\n  /**\n   * Creates download helper.\n   *\n   * @param downloadType A download type. This value will be used as {@link DownloadRequest#type}.\n   * @param uri A {@link Uri}.\n   * @param cacheKey An optional cache key.\n   * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track\n   *     selection needs to be made.\n   * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for\n   *     downloading.\n   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks\n   *     are selected.\n   */\n  public DownloadHelper(\n      String downloadType,\n      Uri uri,\n      @Nullable String cacheKey,\n      @Nullable MediaSource mediaSource,\n      DefaultTrackSelector.Parameters trackSelectorParameters,\n      RendererCapabilities[] rendererCapabilities) {\n    this.downloadType = downloadType;\n    this.uri = uri;\n    this.cacheKey = cacheKey;\n    this.mediaSource = mediaSource;\n    this.trackSelector =\n        new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory());\n    this.rendererCapabilities = rendererCapabilities;\n    this.scratchSet = new SparseIntArray();\n    trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());\n    callbackHandler = new Handler(Util.getLooper());\n    window = new Timeline.Window();\n  }\n\n  /**\n   * Initializes the helper for starting a download.\n   *\n   * @param callback A callback to be notified when preparation completes or fails.\n   * @throws IllegalStateException If the download helper has already been prepared.\n   */\n  public void prepare(Callback callback) {\n    Assertions.checkState(this.callback == null);\n    this.callback = callback;\n    if (mediaSource != null) {\n      mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);\n    } else {\n      callbackHandler.post(() -> callback.onPrepared(this));\n    }\n  }\n\n  /** Releases the helper and all resources it is holding. */\n  public void release() {\n    if (mediaPreparer != null) {\n      mediaPreparer.release();\n    }\n  }\n\n  /**\n   * Returns the manifest, or null if no manifest is loaded. Must not be called until after\n   * preparation completes.\n   */\n  @Nullable\n  public Object getManifest() {\n    if (mediaSource == null) {\n      return null;\n    }\n    assertPreparedWithMedia();\n    return mediaPreparer.timeline.getWindowCount() > 0\n        ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest\n        : null;\n  }\n\n  /**\n   * Returns the number of periods for which media is available. Must not be called until after\n   * preparation completes.\n   */\n  public int getPeriodCount() {\n    if (mediaSource == null) {\n      return 0;\n    }\n    assertPreparedWithMedia();\n    return trackGroupArrays.length;\n  }\n\n  /**\n   * Returns the track groups for the given period. Must not be called until after preparation\n   * completes.\n   *\n   * <p>Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers.\n   *\n   * @param periodIndex The period index.\n   * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream\n   *     content.\n   */\n  public TrackGroupArray getTrackGroups(int periodIndex) {\n    assertPreparedWithMedia();\n    return trackGroupArrays[periodIndex];\n  }\n\n  /**\n   * Returns the mapped track info for the given period. Must not be called until after preparation\n   * completes.\n   *\n   * @param periodIndex The period index.\n   * @return The {@link MappedTrackInfo} for the period.\n   */\n  public MappedTrackInfo getMappedTrackInfo(int periodIndex) {\n    assertPreparedWithMedia();\n    return mappedTrackInfos[periodIndex];\n  }\n\n  /**\n   * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be\n   * called until after preparation completes.\n   *\n   * @param periodIndex The period index.\n   * @param rendererIndex The renderer index.\n   * @return A list of selected {@link TrackSelection track selections}.\n   */\n  public List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {\n    assertPreparedWithMedia();\n    return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];\n  }\n\n  /**\n   * Clears the selection of tracks for a period. Must not be called until after preparation\n   * completes.\n   *\n   * @param periodIndex The period index for which track selections are cleared.\n   */\n  public void clearTrackSelections(int periodIndex) {\n    assertPreparedWithMedia();\n    for (int i = 0; i < rendererCapabilities.length; i++) {\n      trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();\n    }\n  }\n\n  /**\n   * Replaces a selection of tracks to be downloaded. Must not be called until after preparation\n   * completes.\n   *\n   * @param periodIndex The period index for which the track selection is replaced.\n   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new\n   *     selection of tracks.\n   */\n  public void replaceTrackSelections(\n      int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {\n    clearTrackSelections(periodIndex);\n    addTrackSelection(periodIndex, trackSelectorParameters);\n  }\n\n  /**\n   * Adds a selection of tracks to be downloaded. Must not be called until after preparation\n   * completes.\n   *\n   * @param periodIndex The period index this track selection is added for.\n   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new\n   *     selection of tracks.\n   */\n  public void addTrackSelection(\n      int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {\n    assertPreparedWithMedia();\n    trackSelector.setParameters(trackSelectorParameters);\n    runTrackSelection(periodIndex);\n  }\n\n  /**\n   * Convenience method to add selections of tracks for all specified audio languages. If an audio\n   * track in one of the specified languages is not available, the default fallback audio track is\n   * used instead. Must not be called until after preparation completes.\n   *\n   * @param languages A list of audio languages for which tracks should be added to the download\n   *     selection, as IETF BCP 47 conformant tags.\n   */\n  public void addAudioLanguagesToSelection(String... languages) {\n    assertPreparedWithMedia();\n    for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {\n      DefaultTrackSelector.ParametersBuilder parametersBuilder =\n          DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();\n      MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];\n      int rendererCount = mappedTrackInfo.getRendererCount();\n      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {\n        if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {\n          parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);\n        }\n      }\n      for (String language : languages) {\n        parametersBuilder.setPreferredAudioLanguage(language);\n        addTrackSelection(periodIndex, parametersBuilder.build());\n      }\n    }\n  }\n\n  /**\n   * Convenience method to add selections of tracks for all specified text languages. Must not be\n   * called until after preparation completes.\n   *\n   * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be\n   *     selected for downloading if no track with one of the specified {@code languages} is\n   *     available.\n   * @param languages A list of text languages for which tracks should be added to the download\n   *     selection, as IETF BCP 47 conformant tags.\n   */\n  public void addTextLanguagesToSelection(\n      boolean selectUndeterminedTextLanguage, String... languages) {\n    assertPreparedWithMedia();\n    for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {\n      DefaultTrackSelector.ParametersBuilder parametersBuilder =\n          DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();\n      MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];\n      int rendererCount = mappedTrackInfo.getRendererCount();\n      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {\n        if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) {\n          parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);\n        }\n      }\n      parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);\n      for (String language : languages) {\n        parametersBuilder.setPreferredTextLanguage(language);\n        addTrackSelection(periodIndex, parametersBuilder.build());\n      }\n    }\n  }\n\n  /**\n   * Convenience method to add a selection of tracks to be downloaded for a single renderer. Must\n   * not be called until after preparation completes.\n   *\n   * @param periodIndex The period index the track selection is added for.\n   * @param rendererIndex The renderer index the track selection is added for.\n   * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new\n   *     selection of tracks.\n   * @param overrides A list of {@link SelectionOverride SelectionOverrides} to apply to the {@code\n   *     trackSelectorParameters}. If empty, {@code trackSelectorParameters} are used as they are.\n   */\n  public void addTrackSelectionForSingleRenderer(\n      int periodIndex,\n      int rendererIndex,\n      DefaultTrackSelector.Parameters trackSelectorParameters,\n      List<SelectionOverride> overrides) {\n    assertPreparedWithMedia();\n    DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon();\n    for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {\n      builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);\n    }\n    if (overrides.isEmpty()) {\n      addTrackSelection(periodIndex, builder.build());\n    } else {\n      TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex);\n      for (int i = 0; i < overrides.size(); i++) {\n        builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i));\n        addTrackSelection(periodIndex, builder.build());\n      }\n    }\n  }\n\n  /**\n   * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until\n   * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id.\n   *\n   * @param data Application provided data to store in {@link DownloadRequest#data}.\n   * @return The built {@link DownloadRequest}.\n   */\n  public DownloadRequest getDownloadRequest(@Nullable byte[] data) {\n    return getDownloadRequest(uri.toString(), data);\n  }\n\n  /**\n   * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until\n   * after preparation completes.\n   *\n   * @param id The unique content id.\n   * @param data Application provided data to store in {@link DownloadRequest#data}.\n   * @return The built {@link DownloadRequest}.\n   */\n  public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) {\n    if (mediaSource == null) {\n      return new DownloadRequest(\n          id, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data);\n    }\n    assertPreparedWithMedia();\n    List<StreamKey> streamKeys = new ArrayList<>();\n    List<TrackSelection> allSelections = new ArrayList<>();\n    int periodCount = trackSelectionsByPeriodAndRenderer.length;\n    for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {\n      allSelections.clear();\n      int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;\n      for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {\n        allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]);\n      }\n      streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));\n    }\n    return new DownloadRequest(id, downloadType, uri, streamKeys, cacheKey, data);\n  }\n\n  // Initialization of array of Lists.\n  @SuppressWarnings(\"unchecked\")\n  private void onMediaPrepared() {\n    Assertions.checkNotNull(mediaPreparer);\n    Assertions.checkNotNull(mediaPreparer.mediaPeriods);\n    Assertions.checkNotNull(mediaPreparer.timeline);\n    int periodCount = mediaPreparer.mediaPeriods.length;\n    int rendererCount = rendererCapabilities.length;\n    trackSelectionsByPeriodAndRenderer =\n        (List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];\n    immutableTrackSelectionsByPeriodAndRenderer =\n        (List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];\n    for (int i = 0; i < periodCount; i++) {\n      for (int j = 0; j < rendererCount; j++) {\n        trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();\n        immutableTrackSelectionsByPeriodAndRenderer[i][j] =\n            Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);\n      }\n    }\n    trackGroupArrays = new TrackGroupArray[periodCount];\n    mappedTrackInfos = new MappedTrackInfo[periodCount];\n    for (int i = 0; i < periodCount; i++) {\n      trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups();\n      TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);\n      trackSelector.onSelectionActivated(trackSelectorResult.info);\n      mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());\n    }\n    setPreparedWithMedia();\n    Assertions.checkNotNull(callbackHandler)\n        .post(() -> Assertions.checkNotNull(callback).onPrepared(this));\n  }\n\n  private void onMediaPreparationFailed(IOException error) {\n    Assertions.checkNotNull(callbackHandler)\n        .post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error));\n  }\n\n  @RequiresNonNull({\n    \"trackGroupArrays\",\n    \"mappedTrackInfos\",\n    \"trackSelectionsByPeriodAndRenderer\",\n    \"immutableTrackSelectionsByPeriodAndRenderer\",\n    \"mediaPreparer\",\n    \"mediaPreparer.timeline\",\n    \"mediaPreparer.mediaPeriods\"\n  })\n  private void setPreparedWithMedia() {\n    isPreparedWithMedia = true;\n  }\n\n  @EnsuresNonNull({\n    \"trackGroupArrays\",\n    \"mappedTrackInfos\",\n    \"trackSelectionsByPeriodAndRenderer\",\n    \"immutableTrackSelectionsByPeriodAndRenderer\",\n    \"mediaPreparer\",\n    \"mediaPreparer.timeline\",\n    \"mediaPreparer.mediaPeriods\"\n  })\n  @SuppressWarnings(\"nullness:contracts.postcondition.not.satisfied\")\n  private void assertPreparedWithMedia() {\n    Assertions.checkState(isPreparedWithMedia);\n  }\n\n  /**\n   * Runs the track selection for a given period index with the current parameters. The selected\n   * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.\n   */\n  // Intentional reference comparison of track group instances.\n  @SuppressWarnings(\"ReferenceEquality\")\n  @RequiresNonNull({\n    \"trackGroupArrays\",\n    \"trackSelectionsByPeriodAndRenderer\",\n    \"mediaPreparer\",\n    \"mediaPreparer.timeline\"\n  })\n  private TrackSelectorResult runTrackSelection(int periodIndex) {\n    try {\n      TrackSelectorResult trackSelectorResult =\n          trackSelector.selectTracks(\n              rendererCapabilities,\n              trackGroupArrays[periodIndex],\n              new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),\n              mediaPreparer.timeline);\n      for (int i = 0; i < trackSelectorResult.length; i++) {\n        @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i);\n        if (newSelection == null) {\n          continue;\n        }\n        List<TrackSelection> existingSelectionList =\n            trackSelectionsByPeriodAndRenderer[periodIndex][i];\n        boolean mergedWithExistingSelection = false;\n        for (int j = 0; j < existingSelectionList.size(); j++) {\n          TrackSelection existingSelection = existingSelectionList.get(j);\n          if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {\n            // Merge with existing selection.\n            scratchSet.clear();\n            for (int k = 0; k < existingSelection.length(); k++) {\n              scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);\n            }\n            for (int k = 0; k < newSelection.length(); k++) {\n              scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);\n            }\n            int[] mergedTracks = new int[scratchSet.size()];\n            for (int k = 0; k < scratchSet.size(); k++) {\n              mergedTracks[k] = scratchSet.keyAt(k);\n            }\n            existingSelectionList.set(\n                j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));\n            mergedWithExistingSelection = true;\n            break;\n          }\n        }\n        if (!mergedWithExistingSelection) {\n          existingSelectionList.add(newSelection);\n        }\n      }\n      return trackSelectorResult;\n    } catch (ExoPlaybackException e) {\n      // DefaultTrackSelector does not throw exceptions during track selection.\n      throw new UnsupportedOperationException(e);\n    }\n  }\n\n  @Nullable\n  private static Constructor<? extends MediaSourceFactory> getConstructor(String className) {\n    try {\n      // LINT.IfChange\n      Class<? extends MediaSourceFactory> factoryClazz =\n          Class.forName(className).asSubclass(MediaSourceFactory.class);\n      return factoryClazz.getConstructor(Factory.class);\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the respective module.\n      return null;\n    } catch (NoSuchMethodException e) {\n      // Something is wrong with the library or the proguard configuration.\n      throw new IllegalStateException(e);\n    }\n  }\n\n  private static MediaSource createMediaSourceInternal(\n      @Nullable Constructor<? extends MediaSourceFactory> constructor,\n      Uri uri,\n      Factory dataSourceFactory,\n      @Nullable List<StreamKey> streamKeys) {\n    if (constructor == null) {\n      throw new IllegalStateException(\"Module missing to create media source.\");\n    }\n    try {\n      MediaSourceFactory factory = constructor.newInstance(dataSourceFactory);\n      if (streamKeys != null) {\n        factory.setStreamKeys(streamKeys);\n      }\n      return Assertions.checkNotNull(factory.createMediaSource(uri));\n    } catch (Exception e) {\n      throw new IllegalStateException(\"Failed to instantiate media source.\", e);\n    }\n  }\n\n  private static final class MediaPreparer\n      implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback {\n\n    private static final int MESSAGE_PREPARE_SOURCE = 0;\n    private static final int MESSAGE_CHECK_FOR_FAILURE = 1;\n    private static final int MESSAGE_CONTINUE_LOADING = 2;\n    private static final int MESSAGE_RELEASE = 3;\n\n    private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED = 0;\n    private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED = 1;\n\n    private final MediaSource mediaSource;\n    private final DownloadHelper downloadHelper;\n    private final Allocator allocator;\n    private final ArrayList<MediaPeriod> pendingMediaPeriods;\n    private final Handler downloadHelperHandler;\n    private final HandlerThread mediaSourceThread;\n    private final Handler mediaSourceHandler;\n\n    public @MonotonicNonNull Timeline timeline;\n    public MediaPeriod @MonotonicNonNull [] mediaPeriods;\n\n    private boolean released;\n\n    public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) {\n      this.mediaSource = mediaSource;\n      this.downloadHelper = downloadHelper;\n      allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);\n      pendingMediaPeriods = new ArrayList<>();\n      @SuppressWarnings(\"methodref.receiver.bound.invalid\")\n      Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage);\n      this.downloadHelperHandler = downloadThreadHandler;\n      mediaSourceThread = new HandlerThread(\"DownloadHelper\");\n      mediaSourceThread.start();\n      mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this);\n      mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE);\n    }\n\n    public void release() {\n      if (released) {\n        return;\n      }\n      released = true;\n      mediaSourceHandler.sendEmptyMessage(MESSAGE_RELEASE);\n    }\n\n    // Handler.Callback\n\n    @Override\n    public boolean handleMessage(Message msg) {\n      switch (msg.what) {\n        case MESSAGE_PREPARE_SOURCE:\n          mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null);\n          mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);\n          return true;\n        case MESSAGE_CHECK_FOR_FAILURE:\n          try {\n            if (mediaPeriods == null) {\n              mediaSource.maybeThrowSourceInfoRefreshError();\n            } else {\n              for (int i = 0; i < pendingMediaPeriods.size(); i++) {\n                pendingMediaPeriods.get(i).maybeThrowPrepareError();\n              }\n            }\n            mediaSourceHandler.sendEmptyMessageDelayed(\n                MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100);\n          } catch (IOException e) {\n            downloadHelperHandler\n                .obtainMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, /* obj= */ e)\n                .sendToTarget();\n          }\n          return true;\n        case MESSAGE_CONTINUE_LOADING:\n          MediaPeriod mediaPeriod = (MediaPeriod) msg.obj;\n          if (pendingMediaPeriods.contains(mediaPeriod)) {\n            mediaPeriod.continueLoading(/* positionUs= */ 0);\n          }\n          return true;\n        case MESSAGE_RELEASE:\n          if (mediaPeriods != null) {\n            for (MediaPeriod period : mediaPeriods) {\n              mediaSource.releasePeriod(period);\n            }\n          }\n          mediaSource.releaseSource(this);\n          mediaSourceHandler.removeCallbacksAndMessages(null);\n          mediaSourceThread.quit();\n          return true;\n        default:\n          return false;\n      }\n    }\n\n    // MediaSource.MediaSourceCaller implementation.\n\n    @Override\n    public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {\n      if (this.timeline != null) {\n        // Ignore dynamic updates.\n        return;\n      }\n      if (timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).isLive) {\n        downloadHelperHandler\n            .obtainMessage(\n                DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED,\n                /* obj= */ new LiveContentUnsupportedException())\n            .sendToTarget();\n        return;\n      }\n      this.timeline = timeline;\n      mediaPeriods = new MediaPeriod[timeline.getPeriodCount()];\n      for (int i = 0; i < mediaPeriods.length; i++) {\n        MediaPeriod mediaPeriod =\n            mediaSource.createPeriod(\n                new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)),\n                allocator,\n                /* startPositionUs= */ 0);\n        mediaPeriods[i] = mediaPeriod;\n        pendingMediaPeriods.add(mediaPeriod);\n      }\n      for (MediaPeriod mediaPeriod : mediaPeriods) {\n        mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0);\n      }\n    }\n\n    // MediaPeriod.Callback implementation.\n\n    @Override\n    public void onPrepared(MediaPeriod mediaPeriod) {\n      pendingMediaPeriods.remove(mediaPeriod);\n      if (pendingMediaPeriods.isEmpty()) {\n        mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE);\n        downloadHelperHandler.sendEmptyMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED);\n      }\n    }\n\n    @Override\n    public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {\n      if (pendingMediaPeriods.contains(mediaPeriod)) {\n        mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget();\n      }\n    }\n\n    private boolean handleDownloadHelperCallbackMessage(Message msg) {\n      if (released) {\n        // Stale message.\n        return false;\n      }\n      switch (msg.what) {\n        case DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED:\n          downloadHelper.onMediaPrepared();\n          return true;\n        case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:\n          release();\n          downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj));\n          return true;\n        default:\n          return false;\n      }\n    }\n  }\n\n  private static final class DownloadTrackSelection extends BaseTrackSelection {\n\n    private static final class Factory implements TrackSelection.Factory {\n\n      @Override\n      public @NullableType TrackSelection[] createTrackSelections(\n          @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n        @NullableType TrackSelection[] selections = new TrackSelection[definitions.length];\n        for (int i = 0; i < definitions.length; i++) {\n          selections[i] =\n              definitions[i] == null\n                  ? null\n                  : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks);\n        }\n        return selections;\n      }\n    }\n\n    public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) {\n      super(trackGroup, tracks);\n    }\n\n    @Override\n    public int getSelectedIndex() {\n      return 0;\n    }\n\n    @Override\n    public int getSelectionReason() {\n      return C.SELECTION_REASON_UNKNOWN;\n    }\n\n    @Nullable\n    @Override\n    public Object getSelectionData() {\n      return null;\n    }\n\n    @Override\n    public void updateSelectedTrack(\n        long playbackPositionUs,\n        long bufferedDurationUs,\n        long availableDurationUs,\n        List<? extends MediaChunk> queue,\n        MediaChunkIterator[] mediaChunkIterators) {\n      // Do nothing.\n    }\n  }\n\n  private static final class DummyBandwidthMeter implements BandwidthMeter {\n\n    @Override\n    public long getBitrateEstimate() {\n      return 0;\n    }\n\n    @Nullable\n    @Override\n    public TransferListener getTransferListener() {\n      return null;\n    }\n\n    @Override\n    public void addEventListener(Handler eventHandler, EventListener eventListener) {\n      // Do nothing.\n    }\n\n    @Override\n    public void removeEventListener(EventListener eventListener) {\n      // Do nothing.\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.WorkerThread;\nimport java.io.IOException;\n\n/** An index of {@link Download Downloads}. */\n@WorkerThread\npublic interface DownloadIndex {\n\n  /**\n   * Returns the {@link Download} with the given {@code id}, or null.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param id ID of a {@link Download}.\n   * @return The {@link Download} with the given {@code id}, or null if a download state with this\n   *     id doesn't exist.\n   * @throws IOException If an error occurs reading the state.\n   */\n  @Nullable\n  Download getDownload(String id) throws IOException;\n\n  /**\n   * Returns a {@link DownloadCursor} to {@link Download}s with the given {@code states}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param states Returns only the {@link Download}s with this states. If empty, returns all.\n   * @return A cursor to {@link Download}s with the given {@code states}.\n   * @throws IOException If an error occurs reading the state.\n   */\n  DownloadCursor getDownloads(@Download.State int... states) throws IOException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_NONE;\nimport static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_UNKNOWN;\nimport static com.google.android.exoplayer2.offline.Download.STATE_COMPLETED;\nimport static com.google.android.exoplayer2.offline.Download.STATE_DOWNLOADING;\nimport static com.google.android.exoplayer2.offline.Download.STATE_FAILED;\nimport static com.google.android.exoplayer2.offline.Download.STATE_QUEUED;\nimport static com.google.android.exoplayer2.offline.Download.STATE_REMOVING;\nimport static com.google.android.exoplayer2.offline.Download.STATE_RESTARTING;\nimport static com.google.android.exoplayer2.offline.Download.STATE_STOPPED;\nimport static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.scheduler.Requirements;\nimport com.google.android.exoplayer2.scheduler.RequirementsWatcher;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\n/**\n * Manages downloads.\n *\n * <p>Normally a download manager should be accessed via a {@link DownloadService}. When a download\n * manager is used directly instead, downloads will be initially paused and so must be resumed by\n * calling {@link #resumeDownloads()}.\n *\n * <p>A download manager instance must be accessed only from the thread that created it, unless that\n * thread does not have a {@link Looper}. In that case, it must be accessed only from the\n * application's main thread. Registered listeners will be called on the same thread.\n */\npublic final class DownloadManager {\n\n  /** Listener for {@link DownloadManager} events. */\n  public interface Listener {\n\n    /**\n     * Called when all downloads have been restored.\n     *\n     * @param downloadManager The reporting instance.\n     */\n    default void onInitialized(DownloadManager downloadManager) {}\n\n    /**\n     * Called when the state of a download changes.\n     *\n     * @param downloadManager The reporting instance.\n     * @param download The state of the download.\n     */\n    default void onDownloadChanged(DownloadManager downloadManager, Download download) {}\n\n    /**\n     * Called when a download is removed.\n     *\n     * @param downloadManager The reporting instance.\n     * @param download The last state of the download before it was removed.\n     */\n    default void onDownloadRemoved(DownloadManager downloadManager, Download download) {}\n\n    /**\n     * Called when there is no active download left.\n     *\n     * @param downloadManager The reporting instance.\n     */\n    default void onIdle(DownloadManager downloadManager) {}\n\n    /**\n     * Called when the download requirements state changed.\n     *\n     * @param downloadManager The reporting instance.\n     * @param requirements Requirements needed to be met to start downloads.\n     * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not\n     *     met, or 0.\n     */\n    default void onRequirementsStateChanged(\n            DownloadManager downloadManager,\n            Requirements requirements,\n            @Requirements.RequirementFlags int notMetRequirements) {}\n  }\n\n  /** The default maximum number of parallel downloads. */\n  public static final int DEFAULT_MAX_PARALLEL_DOWNLOADS = 3;\n  /** The default minimum number of times a download must be retried before failing. */\n  public static final int DEFAULT_MIN_RETRY_COUNT = 5;\n  /** The default requirement is that the device has network connectivity. */\n  public static final Requirements DEFAULT_REQUIREMENTS = new Requirements(Requirements.NETWORK);\n\n  // Messages posted to the main handler.\n  private static final int MSG_INITIALIZED = 0;\n  private static final int MSG_PROCESSED = 1;\n  private static final int MSG_DOWNLOAD_UPDATE = 2;\n\n  // Messages posted to the background handler.\n  private static final int MSG_INITIALIZE = 0;\n  private static final int MSG_SET_DOWNLOADS_PAUSED = 1;\n  private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2;\n  private static final int MSG_SET_STOP_REASON = 3;\n  private static final int MSG_SET_MAX_PARALLEL_DOWNLOADS = 4;\n  private static final int MSG_SET_MIN_RETRY_COUNT = 5;\n  private static final int MSG_ADD_DOWNLOAD = 6;\n  private static final int MSG_REMOVE_DOWNLOAD = 7;\n  private static final int MSG_REMOVE_ALL_DOWNLOADS = 8;\n  private static final int MSG_TASK_STOPPED = 9;\n  private static final int MSG_CONTENT_LENGTH_CHANGED = 10;\n  private static final int MSG_UPDATE_PROGRESS = 11;\n  private static final int MSG_RELEASE = 12;\n\n  private static final String TAG = \"DownloadManager\";\n\n  private final Context context;\n  private final WritableDownloadIndex downloadIndex;\n  private final Handler mainHandler;\n  private final InternalHandler internalHandler;\n  private final RequirementsWatcher.Listener requirementsListener;\n  private final CopyOnWriteArraySet<Listener> listeners;\n\n  private int pendingMessages;\n  private int activeTaskCount;\n  private boolean initialized;\n  private boolean downloadsPaused;\n  private int maxParallelDownloads;\n  private int minRetryCount;\n  private int notMetRequirements;\n  private List<Download> downloads;\n  private RequirementsWatcher requirementsWatcher;\n\n  /**\n   * Constructs a {@link DownloadManager}.\n   *\n   * @param context Any context.\n   * @param databaseProvider Provides the SQLite database in which downloads are persisted.\n   * @param cache A cache to be used to store downloaded data. The cache should be configured with\n   *     an {@link CacheEvictor} that will not evict downloaded content, for example {@link\n   *     NoOpCacheEvictor}.\n   * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data.\n   */\n  public DownloadManager(\n      Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) {\n    this(\n        context,\n        new DefaultDownloadIndex(databaseProvider),\n        new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory)));\n  }\n\n  /**\n   * Constructs a {@link DownloadManager}.\n   *\n   * @param context Any context.\n   * @param downloadIndex The download index used to hold the download information.\n   * @param downloaderFactory A factory for creating {@link Downloader}s.\n   */\n  public DownloadManager(\n      Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) {\n    this.context = context.getApplicationContext();\n    this.downloadIndex = downloadIndex;\n\n    maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS;\n    minRetryCount = DEFAULT_MIN_RETRY_COUNT;\n    downloadsPaused = true;\n    downloads = Collections.emptyList();\n    listeners = new CopyOnWriteArraySet<>();\n\n    @SuppressWarnings(\"methodref.receiver.bound.invalid\")\n    Handler mainHandler = Util.createHandler(this::handleMainMessage);\n    this.mainHandler = mainHandler;\n    HandlerThread internalThread = new HandlerThread(\"DownloadManager file i/o\");\n    internalThread.start();\n    internalHandler =\n        new InternalHandler(\n            internalThread,\n            downloadIndex,\n            downloaderFactory,\n            mainHandler,\n            maxParallelDownloads,\n            minRetryCount,\n            downloadsPaused);\n\n    @SuppressWarnings(\"methodref.receiver.bound.invalid\")\n    RequirementsWatcher.Listener requirementsListener = this::onRequirementsStateChanged;\n    this.requirementsListener = requirementsListener;\n    requirementsWatcher =\n        new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS);\n    notMetRequirements = requirementsWatcher.start();\n\n    pendingMessages = 1;\n    internalHandler\n        .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  /** Returns whether the manager has completed initialization. */\n  public boolean isInitialized() {\n    return initialized;\n  }\n\n  /**\n   * Returns whether the manager is currently idle. The manager is idle if all downloads are in a\n   * terminal state (i.e. completed or failed), or if no progress can be made (e.g. because the\n   * download requirements are not met).\n   */\n  public boolean isIdle() {\n    return activeTaskCount == 0 && pendingMessages == 0;\n  }\n\n  /**\n   * Returns whether this manager has one or more downloads that are not progressing for the sole\n   * reason that the {@link #getRequirements() Requirements} are not met.\n   */\n  public boolean isWaitingForRequirements() {\n    if (!downloadsPaused && notMetRequirements != 0) {\n      for (int i = 0; i < downloads.size(); i++) {\n        if (downloads.get(i).state == STATE_QUEUED) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Adds a {@link Listener}.\n   *\n   * @param listener The listener to be added.\n   */\n  public void addListener(Listener listener) {\n    listeners.add(listener);\n  }\n\n  /**\n   * Removes a {@link Listener}.\n   *\n   * @param listener The listener to be removed.\n   */\n  public void removeListener(Listener listener) {\n    listeners.remove(listener);\n  }\n\n  /** Returns the requirements needed to be met to progress. */\n  public Requirements getRequirements() {\n    return requirementsWatcher.getRequirements();\n  }\n\n  /**\n   * Returns the requirements needed for downloads to progress that are not currently met.\n   *\n   * @return The not met {@link Requirements.RequirementFlags}, or 0 if all requirements are met.\n   */\n  @Requirements.RequirementFlags\n  public int getNotMetRequirements() {\n    return getRequirements().getNotMetRequirements(context);\n  }\n\n  /**\n   * Sets the requirements that need to be met for downloads to progress.\n   *\n   * @param requirements A {@link Requirements}.\n   */\n  public void setRequirements(Requirements requirements) {\n    if (requirements.equals(requirementsWatcher.getRequirements())) {\n      return;\n    }\n    requirementsWatcher.stop();\n    requirementsWatcher = new RequirementsWatcher(context, requirementsListener, requirements);\n    int notMetRequirements = requirementsWatcher.start();\n    onRequirementsStateChanged(requirementsWatcher, notMetRequirements);\n  }\n\n  /** Returns the maximum number of parallel downloads. */\n  public int getMaxParallelDownloads() {\n    return maxParallelDownloads;\n  }\n\n  /**\n   * Sets the maximum number of parallel downloads.\n   *\n   * @param maxParallelDownloads The maximum number of parallel downloads. Must be greater than 0.\n   */\n  public void setMaxParallelDownloads(int maxParallelDownloads) {\n    Assertions.checkArgument(maxParallelDownloads > 0);\n    if (this.maxParallelDownloads == maxParallelDownloads) {\n      return;\n    }\n    this.maxParallelDownloads = maxParallelDownloads;\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_MAX_PARALLEL_DOWNLOADS, maxParallelDownloads, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  /**\n   * Returns the minimum number of times that a download will be retried. A download will fail if\n   * the specified number of retries is exceeded without any progress being made.\n   */\n  public int getMinRetryCount() {\n    return minRetryCount;\n  }\n\n  /**\n   * Sets the minimum number of times that a download will be retried. A download will fail if the\n   * specified number of retries is exceeded without any progress being made.\n   *\n   * @param minRetryCount The minimum number of times that a download will be retried.\n   */\n  public void setMinRetryCount(int minRetryCount) {\n    Assertions.checkArgument(minRetryCount >= 0);\n    if (this.minRetryCount == minRetryCount) {\n      return;\n    }\n    this.minRetryCount = minRetryCount;\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_MIN_RETRY_COUNT, minRetryCount, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  /** Returns the used {@link DownloadIndex}. */\n  public DownloadIndex getDownloadIndex() {\n    return downloadIndex;\n  }\n\n  /**\n   * Returns current downloads. Downloads that are in terminal states (i.e. completed or failed) are\n   * not included. To query all downloads including those in terminal states, use {@link\n   * #getDownloadIndex()} instead.\n   */\n  public List<Download> getCurrentDownloads() {\n    return downloads;\n  }\n\n  /** Returns whether downloads are currently paused. */\n  public boolean getDownloadsPaused() {\n    return downloadsPaused;\n  }\n\n  /**\n   * Resumes downloads.\n   *\n   * <p>If the {@link #setRequirements(Requirements) Requirements} are met up to {@link\n   * #getMaxParallelDownloads() maxParallelDownloads} will be started, excluding those with non-zero\n   * {@link Download#stopReason stopReasons}.\n   */\n  public void resumeDownloads() {\n    if (!downloadsPaused) {\n      return;\n    }\n    downloadsPaused = false;\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 0, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  /**\n   * Pauses downloads. Downloads that would otherwise be making progress transition to {@link\n   * Download#STATE_QUEUED}.\n   */\n  public void pauseDownloads() {\n    if (downloadsPaused) {\n      return;\n    }\n    downloadsPaused = true;\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 1, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  /**\n   * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link\n   * Download#STOP_REASON_NONE}.\n   *\n   * @param id The content id of the download to update, or {@code null} to set the stop reason for\n   *     all downloads.\n   * @param stopReason The stop reason, or {@link Download#STOP_REASON_NONE}.\n   */\n  public void setStopReason(@Nullable String id, int stopReason) {\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_STOP_REASON, stopReason, /* unused */ 0, id)\n        .sendToTarget();\n  }\n\n  /**\n   * Adds a download defined by the given request.\n   *\n   * @param request The download request.\n   */\n  public void addDownload(DownloadRequest request) {\n    addDownload(request, STOP_REASON_NONE);\n  }\n\n  /**\n   * Adds a download defined by the given request and with the specified stop reason.\n   *\n   * @param request The download request.\n   * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE}\n   *     if the download should be started.\n   */\n  public void addDownload(DownloadRequest request, int stopReason) {\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_ADD_DOWNLOAD, stopReason, /* unused */ 0, request)\n        .sendToTarget();\n  }\n\n  /**\n   * Cancels the download with the {@code id} and removes all downloaded data.\n   *\n   * @param id The unique content id of the download to be started.\n   */\n  public void removeDownload(String id) {\n    pendingMessages++;\n    internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget();\n  }\n\n  /** Cancels all pending downloads and removes all downloaded data. */\n  public void removeAllDownloads() {\n    pendingMessages++;\n    internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget();\n  }\n\n  /**\n   * Stops the downloads and releases resources. Waits until the downloads are persisted to the\n   * download index. The manager must not be accessed after this method has been called.\n   */\n  public void release() {\n    synchronized (internalHandler) {\n      if (internalHandler.released) {\n        return;\n      }\n      internalHandler.sendEmptyMessage(MSG_RELEASE);\n      boolean wasInterrupted = false;\n      while (!internalHandler.released) {\n        try {\n          internalHandler.wait();\n        } catch (InterruptedException e) {\n          wasInterrupted = true;\n        }\n      }\n      if (wasInterrupted) {\n        // Restore the interrupted status.\n        Thread.currentThread().interrupt();\n      }\n      mainHandler.removeCallbacksAndMessages(/* token= */ null);\n      // Reset state.\n      downloads = Collections.emptyList();\n      pendingMessages = 0;\n      activeTaskCount = 0;\n      initialized = false;\n    }\n  }\n\n  private void onRequirementsStateChanged(\n      RequirementsWatcher requirementsWatcher,\n      @Requirements.RequirementFlags int notMetRequirements) {\n    Requirements requirements = requirementsWatcher.getRequirements();\n    for (Listener listener : listeners) {\n      listener.onRequirementsStateChanged(this, requirements, notMetRequirements);\n    }\n    if (this.notMetRequirements == notMetRequirements) {\n      return;\n    }\n    this.notMetRequirements = notMetRequirements;\n    pendingMessages++;\n    internalHandler\n        .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0)\n        .sendToTarget();\n  }\n\n  // Main thread message handling.\n\n  @SuppressWarnings(\"unchecked\")\n  private boolean handleMainMessage(Message message) {\n    switch (message.what) {\n      case MSG_INITIALIZED:\n        List<Download> downloads = (List<Download>) message.obj;\n        onInitialized(downloads);\n        break;\n      case MSG_DOWNLOAD_UPDATE:\n        DownloadUpdate update = (DownloadUpdate) message.obj;\n        onDownloadUpdate(update);\n        break;\n      case MSG_PROCESSED:\n        int processedMessageCount = message.arg1;\n        int activeTaskCount = message.arg2;\n        onMessageProcessed(processedMessageCount, activeTaskCount);\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n    return true;\n  }\n\n  private void onInitialized(List<Download> downloads) {\n    initialized = true;\n    this.downloads = Collections.unmodifiableList(downloads);\n    for (Listener listener : listeners) {\n      listener.onInitialized(DownloadManager.this);\n    }\n  }\n\n  private void onDownloadUpdate(DownloadUpdate update) {\n    downloads = Collections.unmodifiableList(update.downloads);\n    Download updatedDownload = update.download;\n    if (update.isRemove) {\n      for (Listener listener : listeners) {\n        listener.onDownloadRemoved(this, updatedDownload);\n      }\n    } else {\n      for (Listener listener : listeners) {\n        listener.onDownloadChanged(this, updatedDownload);\n      }\n    }\n  }\n\n  private void onMessageProcessed(int processedMessageCount, int activeTaskCount) {\n    this.pendingMessages -= processedMessageCount;\n    this.activeTaskCount = activeTaskCount;\n    if (isIdle()) {\n      for (Listener listener : listeners) {\n        listener.onIdle(this);\n      }\n    }\n  }\n\n  /* package */ static Download mergeRequest(\n      Download download, DownloadRequest request, int stopReason, long nowMs) {\n    @Download.State int state = download.state;\n    // Treat the merge as creating a new download if we're currently removing the existing one, or\n    // if the existing download is in a terminal state. Else treat the merge as updating the\n    // existing download.\n    long startTimeMs =\n        state == STATE_REMOVING || download.isTerminalState() ? nowMs : download.startTimeMs;\n    if (state == STATE_REMOVING || state == STATE_RESTARTING) {\n      state = STATE_RESTARTING;\n    } else if (stopReason != STOP_REASON_NONE) {\n      state = STATE_STOPPED;\n    } else {\n      state = STATE_QUEUED;\n    }\n    return new Download(\n        download.request.copyWithMergedRequest(request),\n        state,\n        startTimeMs,\n        /* updateTimeMs= */ nowMs,\n        /* contentLength= */ C.LENGTH_UNSET,\n        stopReason,\n        FAILURE_REASON_NONE);\n  }\n\n  private static final class InternalHandler extends Handler {\n\n    private static final int UPDATE_PROGRESS_INTERVAL_MS = 5000;\n\n    public boolean released;\n\n    private final HandlerThread thread;\n    private final WritableDownloadIndex downloadIndex;\n    private final DownloaderFactory downloaderFactory;\n    private final Handler mainHandler;\n    private final ArrayList<Download> downloads;\n    private final HashMap<String, Task> activeTasks;\n\n    @Requirements.RequirementFlags private int notMetRequirements;\n    private boolean downloadsPaused;\n    private int maxParallelDownloads;\n    private int minRetryCount;\n    private int activeDownloadTaskCount;\n\n    public InternalHandler(\n        HandlerThread thread,\n        WritableDownloadIndex downloadIndex,\n        DownloaderFactory downloaderFactory,\n        Handler mainHandler,\n        int maxParallelDownloads,\n        int minRetryCount,\n        boolean downloadsPaused) {\n      super(thread.getLooper());\n      this.thread = thread;\n      this.downloadIndex = downloadIndex;\n      this.downloaderFactory = downloaderFactory;\n      this.mainHandler = mainHandler;\n      this.maxParallelDownloads = maxParallelDownloads;\n      this.minRetryCount = minRetryCount;\n      this.downloadsPaused = downloadsPaused;\n      downloads = new ArrayList<>();\n      activeTasks = new HashMap<>();\n    }\n\n    @Override\n    public void handleMessage(Message message) {\n      boolean processedExternalMessage = true;\n      switch (message.what) {\n        case MSG_INITIALIZE:\n          int notMetRequirements = message.arg1;\n          initialize(notMetRequirements);\n          break;\n        case MSG_SET_DOWNLOADS_PAUSED:\n          boolean downloadsPaused = message.arg1 != 0;\n          setDownloadsPaused(downloadsPaused);\n          break;\n        case MSG_SET_NOT_MET_REQUIREMENTS:\n          notMetRequirements = message.arg1;\n          setNotMetRequirements(notMetRequirements);\n          break;\n        case MSG_SET_STOP_REASON:\n          String id = (String) message.obj;\n          int stopReason = message.arg1;\n          setStopReason(id, stopReason);\n          break;\n        case MSG_SET_MAX_PARALLEL_DOWNLOADS:\n          int maxParallelDownloads = message.arg1;\n          setMaxParallelDownloads(maxParallelDownloads);\n          break;\n        case MSG_SET_MIN_RETRY_COUNT:\n          int minRetryCount = message.arg1;\n          setMinRetryCount(minRetryCount);\n          break;\n        case MSG_ADD_DOWNLOAD:\n          DownloadRequest request = (DownloadRequest) message.obj;\n          stopReason = message.arg1;\n          addDownload(request, stopReason);\n          break;\n        case MSG_REMOVE_DOWNLOAD:\n          id = (String) message.obj;\n          removeDownload(id);\n          break;\n        case MSG_REMOVE_ALL_DOWNLOADS:\n          removeAllDownloads();\n          break;\n        case MSG_TASK_STOPPED:\n          Task task = (Task) message.obj;\n          onTaskStopped(task);\n          processedExternalMessage = false; // This message is posted internally.\n          break;\n        case MSG_CONTENT_LENGTH_CHANGED:\n          task = (Task) message.obj;\n          onContentLengthChanged(task);\n          return; // No need to post back to mainHandler.\n        case MSG_UPDATE_PROGRESS:\n          updateProgress();\n          return; // No need to post back to mainHandler.\n        case MSG_RELEASE:\n          release();\n          return; // No need to post back to mainHandler.\n        default:\n          throw new IllegalStateException();\n      }\n      mainHandler\n          .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, activeTasks.size())\n          .sendToTarget();\n    }\n\n    private void initialize(int notMetRequirements) {\n      this.notMetRequirements = notMetRequirements;\n      DownloadCursor cursor = null;\n      try {\n        downloadIndex.setDownloadingStatesToQueued();\n        cursor =\n            downloadIndex.getDownloads(\n                STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING);\n        while (cursor.moveToNext()) {\n          downloads.add(cursor.getDownload());\n        }\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to load index.\", e);\n        downloads.clear();\n      } finally {\n        Util.closeQuietly(cursor);\n      }\n      // A copy must be used for the message to ensure that subsequent changes to the downloads list\n      // are not visible to the main thread when it processes the message.\n      ArrayList<Download> downloadsForMessage = new ArrayList<>(downloads);\n      mainHandler.obtainMessage(MSG_INITIALIZED, downloadsForMessage).sendToTarget();\n      syncTasks();\n    }\n\n    private void setDownloadsPaused(boolean downloadsPaused) {\n      this.downloadsPaused = downloadsPaused;\n      syncTasks();\n    }\n\n    private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {\n      this.notMetRequirements = notMetRequirements;\n      syncTasks();\n    }\n\n    private void setStopReason(@Nullable String id, int stopReason) {\n      if (id == null) {\n        for (int i = 0; i < downloads.size(); i++) {\n          setStopReason(downloads.get(i), stopReason);\n        }\n        try {\n          // Set the stop reason for downloads in terminal states as well.\n          downloadIndex.setStopReason(stopReason);\n        } catch (IOException e) {\n          Log.e(TAG, \"Failed to set manual stop reason\", e);\n        }\n      } else {\n        @Nullable Download download = getDownload(id, /* loadFromIndex= */ false);\n        if (download != null) {\n          setStopReason(download, stopReason);\n        } else {\n          try {\n            // Set the stop reason if the download is in a terminal state.\n            downloadIndex.setStopReason(id, stopReason);\n          } catch (IOException e) {\n            Log.e(TAG, \"Failed to set manual stop reason: \" + id, e);\n          }\n        }\n      }\n      syncTasks();\n    }\n\n    private void setStopReason(Download download, int stopReason) {\n      if (stopReason == STOP_REASON_NONE) {\n        if (download.state == STATE_STOPPED) {\n          putDownloadWithState(download, STATE_QUEUED);\n        }\n      } else if (stopReason != download.stopReason) {\n        @Download.State int state = download.state;\n        if (state == STATE_QUEUED || state == STATE_DOWNLOADING) {\n          state = STATE_STOPPED;\n        }\n        putDownload(\n            new Download(\n                download.request,\n                state,\n                download.startTimeMs,\n                /* updateTimeMs= */ System.currentTimeMillis(),\n                download.contentLength,\n                stopReason,\n                FAILURE_REASON_NONE,\n                download.progress));\n      }\n    }\n\n    private void setMaxParallelDownloads(int maxParallelDownloads) {\n      this.maxParallelDownloads = maxParallelDownloads;\n      syncTasks();\n    }\n\n    private void setMinRetryCount(int minRetryCount) {\n      this.minRetryCount = minRetryCount;\n    }\n\n    private void addDownload(DownloadRequest request, int stopReason) {\n      @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true);\n      long nowMs = System.currentTimeMillis();\n      if (download != null) {\n        putDownload(mergeRequest(download, request, stopReason, nowMs));\n      } else {\n        putDownload(\n            new Download(\n                request,\n                stopReason != STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED,\n                /* startTimeMs= */ nowMs,\n                /* updateTimeMs= */ nowMs,\n                /* contentLength= */ C.LENGTH_UNSET,\n                stopReason,\n                FAILURE_REASON_NONE));\n      }\n      syncTasks();\n    }\n\n    private void removeDownload(String id) {\n      @Nullable Download download = getDownload(id, /* loadFromIndex= */ true);\n      if (download == null) {\n        Log.e(TAG, \"Failed to remove nonexistent download: \" + id);\n        return;\n      }\n      putDownloadWithState(download, STATE_REMOVING);\n      syncTasks();\n    }\n\n    private void removeAllDownloads() {\n      List<Download> terminalDownloads = new ArrayList<>();\n      try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) {\n        while (cursor.moveToNext()) {\n          terminalDownloads.add(cursor.getDownload());\n        }\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to load downloads.\");\n      }\n      for (int i = 0; i < downloads.size(); i++) {\n        downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING));\n      }\n      for (int i = 0; i < terminalDownloads.size(); i++) {\n        downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING));\n      }\n      Collections.sort(downloads, InternalHandler::compareStartTimes);\n      try {\n        downloadIndex.setStatesToRemoving();\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to update index.\", e);\n      }\n      ArrayList<Download> updateList = new ArrayList<>(downloads);\n      for (int i = 0; i < downloads.size(); i++) {\n        DownloadUpdate update =\n            new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList);\n        mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();\n      }\n      syncTasks();\n    }\n\n    private void release() {\n      for (Task task : activeTasks.values()) {\n        task.cancel(/* released= */ true);\n      }\n      try {\n        downloadIndex.setDownloadingStatesToQueued();\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to update index.\", e);\n      }\n      downloads.clear();\n      thread.quit();\n      synchronized (this) {\n        released = true;\n        notifyAll();\n      }\n    }\n\n    // Start and cancel tasks based on the current download and manager states.\n\n    private void syncTasks() {\n      int accumulatingDownloadTaskCount = 0;\n      for (int i = 0; i < downloads.size(); i++) {\n        Download download = downloads.get(i);\n        @Nullable Task activeTask = activeTasks.get(download.request.id);\n        switch (download.state) {\n          case STATE_STOPPED:\n            syncStoppedDownload(activeTask);\n            break;\n          case STATE_QUEUED:\n            activeTask = syncQueuedDownload(activeTask, download);\n            break;\n          case STATE_DOWNLOADING:\n            Assertions.checkNotNull(activeTask);\n            syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount);\n            break;\n          case STATE_REMOVING:\n          case STATE_RESTARTING:\n            syncRemovingDownload(activeTask, download);\n            break;\n          case STATE_COMPLETED:\n          case STATE_FAILED:\n          default:\n            throw new IllegalStateException();\n        }\n        if (activeTask != null && !activeTask.isRemove) {\n          accumulatingDownloadTaskCount++;\n        }\n      }\n    }\n\n    private void syncStoppedDownload(@Nullable Task activeTask) {\n      if (activeTask != null) {\n        // We have a task, which must be a download task. Cancel it.\n        Assertions.checkState(!activeTask.isRemove);\n        activeTask.cancel(/* released= */ false);\n      }\n    }\n\n    @Nullable\n    @CheckResult\n    private Task syncQueuedDownload(@Nullable Task activeTask, Download download) {\n      if (activeTask != null) {\n        // We have a task, which must be a download task. If the download state is queued we need to\n        // cancel it and start a new one, since a new request has been merged into the download.\n        Assertions.checkState(!activeTask.isRemove);\n        activeTask.cancel(/* released= */ false);\n        return activeTask;\n      }\n\n      if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) {\n        return null;\n      }\n\n      // We can start a download task.\n      download = putDownloadWithState(download, STATE_DOWNLOADING);\n      Downloader downloader = downloaderFactory.createDownloader(download.request);\n      activeTask =\n          new Task(\n              download.request,\n              downloader,\n              download.progress,\n              /* isRemove= */ false,\n              minRetryCount,\n              /* internalHandler= */ this);\n      activeTasks.put(download.request.id, activeTask);\n      if (activeDownloadTaskCount++ == 0) {\n        sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS);\n      }\n      activeTask.start();\n      return activeTask;\n    }\n\n    private void syncDownloadingDownload(\n        Task activeTask, Download download, int accumulatingDownloadTaskCount) {\n      Assertions.checkState(!activeTask.isRemove);\n      if (!canDownloadsRun() || accumulatingDownloadTaskCount >= maxParallelDownloads) {\n        putDownloadWithState(download, STATE_QUEUED);\n        activeTask.cancel(/* released= */ false);\n      }\n    }\n\n    private void syncRemovingDownload(@Nullable Task activeTask, Download download) {\n      if (activeTask != null) {\n        if (!activeTask.isRemove) {\n          // Cancel the downloading task.\n          activeTask.cancel(/* released= */ false);\n        }\n        // The activeTask is either a remove task, or a downloading task that we just cancelled. In\n        // the latter case we need to wait for the task to stop before we start a remove task.\n        return;\n      }\n\n      // We can start a remove task.\n      Downloader downloader = downloaderFactory.createDownloader(download.request);\n      activeTask =\n          new Task(\n              download.request,\n              downloader,\n              download.progress,\n              /* isRemove= */ true,\n              minRetryCount,\n              /* internalHandler= */ this);\n      activeTasks.put(download.request.id, activeTask);\n      activeTask.start();\n    }\n\n    // Task event processing.\n\n    private void onContentLengthChanged(Task task) {\n      String downloadId = task.request.id;\n      long contentLength = task.contentLength;\n      Download download =\n          Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false));\n      if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) {\n        return;\n      }\n      putDownload(\n          new Download(\n              download.request,\n              download.state,\n              download.startTimeMs,\n              /* updateTimeMs= */ System.currentTimeMillis(),\n              contentLength,\n              download.stopReason,\n              download.failureReason,\n              download.progress));\n    }\n\n    private void onTaskStopped(Task task) {\n      String downloadId = task.request.id;\n      activeTasks.remove(downloadId);\n\n      boolean isRemove = task.isRemove;\n      if (!isRemove && --activeDownloadTaskCount == 0) {\n        removeMessages(MSG_UPDATE_PROGRESS);\n      }\n\n      if (task.isCanceled) {\n        syncTasks();\n        return;\n      }\n\n      @Nullable Throwable finalError = task.finalError;\n      if (finalError != null) {\n        Log.e(TAG, \"Task failed: \" + task.request + \", \" + isRemove, finalError);\n      }\n\n      Download download =\n          Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false));\n      switch (download.state) {\n        case STATE_DOWNLOADING:\n          Assertions.checkState(!isRemove);\n          onDownloadTaskStopped(download, finalError);\n          break;\n        case STATE_REMOVING:\n        case STATE_RESTARTING:\n          Assertions.checkState(isRemove);\n          onRemoveTaskStopped(download);\n          break;\n        case STATE_QUEUED:\n        case STATE_STOPPED:\n        case STATE_COMPLETED:\n        case STATE_FAILED:\n        default:\n          throw new IllegalStateException();\n      }\n\n      syncTasks();\n    }\n\n    private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) {\n      download =\n          new Download(\n              download.request,\n              finalError == null ? STATE_COMPLETED : STATE_FAILED,\n              download.startTimeMs,\n              /* updateTimeMs= */ System.currentTimeMillis(),\n              download.contentLength,\n              download.stopReason,\n              finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN,\n              download.progress);\n      // The download is now in a terminal state, so should not be in the downloads list.\n      downloads.remove(getDownloadIndex(download.request.id));\n      // We still need to update the download index and main thread.\n      try {\n        downloadIndex.putDownload(download);\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to update index.\", e);\n      }\n      DownloadUpdate update =\n          new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads));\n      mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();\n    }\n\n    private void onRemoveTaskStopped(Download download) {\n      if (download.state == STATE_RESTARTING) {\n        putDownloadWithState(\n            download, download.stopReason == STOP_REASON_NONE ? STATE_QUEUED : STATE_STOPPED);\n        syncTasks();\n      } else {\n        int removeIndex = getDownloadIndex(download.request.id);\n        downloads.remove(removeIndex);\n        try {\n          downloadIndex.removeDownload(download.request.id);\n        } catch (IOException e) {\n          Log.e(TAG, \"Failed to remove from database\");\n        }\n        DownloadUpdate update =\n            new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads));\n        mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();\n      }\n    }\n\n    // Progress updates.\n\n    private void updateProgress() {\n      for (int i = 0; i < downloads.size(); i++) {\n        Download download = downloads.get(i);\n        if (download.state == STATE_DOWNLOADING) {\n          try {\n            downloadIndex.putDownload(download);\n          } catch (IOException e) {\n            Log.e(TAG, \"Failed to update index.\", e);\n          }\n        }\n      }\n      sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS);\n    }\n\n    // Helper methods.\n\n    private boolean canDownloadsRun() {\n      return !downloadsPaused && notMetRequirements == 0;\n    }\n\n    private Download putDownloadWithState(Download download, @Download.State int state) {\n      // Downloads in terminal states shouldn't be in the downloads list. This method cannot be used\n      // to set STATE_STOPPED either, because it doesn't have a stopReason argument.\n      Assertions.checkState(\n          state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED);\n      return putDownload(copyDownloadWithState(download, state));\n    }\n\n    private Download putDownload(Download download) {\n      // Downloads in terminal states shouldn't be in the downloads list.\n      Assertions.checkState(download.state != STATE_COMPLETED && download.state != STATE_FAILED);\n      int changedIndex = getDownloadIndex(download.request.id);\n      if (changedIndex == C.INDEX_UNSET) {\n        downloads.add(download);\n        Collections.sort(downloads, InternalHandler::compareStartTimes);\n      } else {\n        boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs;\n        downloads.set(changedIndex, download);\n        if (needsSort) {\n          Collections.sort(downloads, InternalHandler::compareStartTimes);\n        }\n      }\n      try {\n        downloadIndex.putDownload(download);\n      } catch (IOException e) {\n        Log.e(TAG, \"Failed to update index.\", e);\n      }\n      DownloadUpdate update =\n          new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads));\n      mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();\n      return download;\n    }\n\n    @Nullable\n    private Download getDownload(String id, boolean loadFromIndex) {\n      int index = getDownloadIndex(id);\n      if (index != C.INDEX_UNSET) {\n        return downloads.get(index);\n      }\n      if (loadFromIndex) {\n        try {\n          return downloadIndex.getDownload(id);\n        } catch (IOException e) {\n          Log.e(TAG, \"Failed to load download: \" + id, e);\n        }\n      }\n      return null;\n    }\n\n    private int getDownloadIndex(String id) {\n      for (int i = 0; i < downloads.size(); i++) {\n        Download download = downloads.get(i);\n        if (download.request.id.equals(id)) {\n          return i;\n        }\n      }\n      return C.INDEX_UNSET;\n    }\n\n    private static Download copyDownloadWithState(Download download, @Download.State int state) {\n      return new Download(\n          download.request,\n          state,\n          download.startTimeMs,\n          /* updateTimeMs= */ System.currentTimeMillis(),\n          download.contentLength,\n          /* stopReason= */ 0,\n          FAILURE_REASON_NONE,\n          download.progress);\n    }\n\n    private static int compareStartTimes(Download first, Download second) {\n      return Util.compareLong(first.startTimeMs, second.startTimeMs);\n    }\n  }\n\n  private static class Task extends Thread implements Downloader.ProgressListener {\n\n    private final DownloadRequest request;\n    private final Downloader downloader;\n    private final DownloadProgress downloadProgress;\n    private final boolean isRemove;\n    private final int minRetryCount;\n\n    @Nullable private volatile InternalHandler internalHandler;\n    private volatile boolean isCanceled;\n    @Nullable private Throwable finalError;\n\n    private long contentLength;\n\n    private Task(\n        DownloadRequest request,\n        Downloader downloader,\n        DownloadProgress downloadProgress,\n        boolean isRemove,\n        int minRetryCount,\n        InternalHandler internalHandler) {\n      this.request = request;\n      this.downloader = downloader;\n      this.downloadProgress = downloadProgress;\n      this.isRemove = isRemove;\n      this.minRetryCount = minRetryCount;\n      this.internalHandler = internalHandler;\n      contentLength = C.LENGTH_UNSET;\n    }\n\n    @SuppressWarnings(\"nullness:assignment.type.incompatible\")\n    public void cancel(boolean released) {\n      if (released) {\n        // Download threads are GC roots for as long as they're running. The time taken for\n        // cancellation to complete depends on the implementation of the downloader being used. We\n        // null the handler reference here so that it doesn't prevent garbage collection of the\n        // download manager whilst cancellation is ongoing.\n        internalHandler = null;\n      }\n      if (!isCanceled) {\n        isCanceled = true;\n        downloader.cancel();\n        interrupt();\n      }\n    }\n\n    // Methods running on download thread.\n\n    @Override\n    public void run() {\n      try {\n        if (isRemove) {\n          downloader.remove();\n        } else {\n          int errorCount = 0;\n          long errorPosition = C.LENGTH_UNSET;\n          while (!isCanceled) {\n            try {\n              downloader.download(/* progressListener= */ this);\n              break;\n            } catch (IOException e) {\n              if (!isCanceled) {\n                long bytesDownloaded = downloadProgress.bytesDownloaded;\n                if (bytesDownloaded != errorPosition) {\n                  errorPosition = bytesDownloaded;\n                  errorCount = 0;\n                }\n                if (++errorCount > minRetryCount) {\n                  throw e;\n                }\n                Thread.sleep(getRetryDelayMillis(errorCount));\n              }\n            }\n          }\n        }\n      } catch (Throwable e) {\n        finalError = e;\n      }\n      @Nullable Handler internalHandler = this.internalHandler;\n      if (internalHandler != null) {\n        internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget();\n      }\n    }\n\n    @Override\n    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {\n      downloadProgress.bytesDownloaded = bytesDownloaded;\n      downloadProgress.percentDownloaded = percentDownloaded;\n      if (contentLength != this.contentLength) {\n        this.contentLength = contentLength;\n        @Nullable Handler internalHandler = this.internalHandler;\n        if (internalHandler != null) {\n          internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget();\n        }\n      }\n    }\n\n    private static int getRetryDelayMillis(int errorCount) {\n      return Math.min((errorCount - 1) * 1000, 5000);\n    }\n  }\n\n  private static final class DownloadUpdate {\n\n    public final Download download;\n    public final boolean isRemove;\n    public final List<Download> downloads;\n\n    public DownloadUpdate(Download download, boolean isRemove, List<Download> downloads) {\n      this.download = download;\n      this.isRemove = isRemove;\n      this.downloads = downloads;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport com.google.android.exoplayer2.C;\n\n/** Mutable {@link Download} progress. */\npublic class DownloadProgress {\n\n  /** The number of bytes that have been downloaded. */\n  public long bytesDownloaded;\n\n  /** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */\n  public float percentDownloaded;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.net.Uri;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Defines content to be downloaded. */\npublic final class DownloadRequest implements Parcelable {\n\n  /** Thrown when the encoded request data belongs to an unsupported request type. */\n  public static class UnsupportedRequestException extends IOException {}\n\n  /** Type for progressive downloads. */\n  public static final String TYPE_PROGRESSIVE = \"progressive\";\n  /** Type for DASH downloads. */\n  public static final String TYPE_DASH = \"dash\";\n  /** Type for HLS downloads. */\n  public static final String TYPE_HLS = \"hls\";\n  /** Type for SmoothStreaming downloads. */\n  public static final String TYPE_SS = \"ss\";\n\n  /** The unique content id. */\n  public final String id;\n  /** The type of the request. */\n  public final String type;\n  /** The uri being downloaded. */\n  public final Uri uri;\n  /** Stream keys to be downloaded. If empty, all streams will be downloaded. */\n  public final List<StreamKey> streamKeys;\n  /**\n   * Custom key for cache indexing, or null. Must be null for DASH, HLS and SmoothStreaming\n   * downloads.\n   */\n  @Nullable public final String customCacheKey;\n  /** Application defined data associated with the download. May be empty. */\n  public final byte[] data;\n\n  /**\n   * @param id See {@link #id}.\n   * @param type See {@link #type}.\n   * @param uri See {@link #uri}.\n   * @param streamKeys See {@link #streamKeys}.\n   * @param customCacheKey See {@link #customCacheKey}.\n   * @param data See {@link #data}.\n   */\n  public DownloadRequest(\n      String id,\n      String type,\n      Uri uri,\n      List<StreamKey> streamKeys,\n      @Nullable String customCacheKey,\n      @Nullable byte[] data) {\n    if (TYPE_DASH.equals(type) || TYPE_HLS.equals(type) || TYPE_SS.equals(type)) {\n      Assertions.checkArgument(\n          customCacheKey == null, \"customCacheKey must be null for type: \" + type);\n    }\n    this.id = id;\n    this.type = type;\n    this.uri = uri;\n    ArrayList<StreamKey> mutableKeys = new ArrayList<>(streamKeys);\n    Collections.sort(mutableKeys);\n    this.streamKeys = Collections.unmodifiableList(mutableKeys);\n    this.customCacheKey = customCacheKey;\n    this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;\n  }\n\n  /* package */ DownloadRequest(Parcel in) {\n    id = castNonNull(in.readString());\n    type = castNonNull(in.readString());\n    uri = Uri.parse(castNonNull(in.readString()));\n    int streamKeyCount = in.readInt();\n    ArrayList<StreamKey> mutableStreamKeys = new ArrayList<>(streamKeyCount);\n    for (int i = 0; i < streamKeyCount; i++) {\n      mutableStreamKeys.add(in.readParcelable(StreamKey.class.getClassLoader()));\n    }\n    streamKeys = Collections.unmodifiableList(mutableStreamKeys);\n    customCacheKey = in.readString();\n    data = new byte[in.readInt()];\n    in.readByteArray(data);\n  }\n\n  /**\n   * Returns a copy with the specified ID.\n   *\n   * @param id The ID of the copy.\n   * @return The copy with the specified ID.\n   */\n  public DownloadRequest copyWithId(String id) {\n    return new DownloadRequest(id, type, uri, streamKeys, customCacheKey, data);\n  }\n\n  /**\n   * Returns the result of merging {@code newRequest} into this request. The requests must have the\n   * same {@link #id} and {@link #type}.\n   *\n   * <p>If the requests have different {@link #uri}, {@link #customCacheKey} and {@link #data}\n   * values, then those from the request being merged are included in the result.\n   *\n   * @param newRequest The request being merged.\n   * @return The merged result.\n   * @throws IllegalArgumentException If the requests do not have the same {@link #id} and {@link\n   *     #type}.\n   */\n  public DownloadRequest copyWithMergedRequest(DownloadRequest newRequest) {\n    Assertions.checkArgument(id.equals(newRequest.id));\n    Assertions.checkArgument(type.equals(newRequest.type));\n    List<StreamKey> mergedKeys;\n    if (streamKeys.isEmpty() || newRequest.streamKeys.isEmpty()) {\n      // If either streamKeys is empty then all streams should be downloaded.\n      mergedKeys = Collections.emptyList();\n    } else {\n      mergedKeys = new ArrayList<>(streamKeys);\n      for (int i = 0; i < newRequest.streamKeys.size(); i++) {\n        StreamKey newKey = newRequest.streamKeys.get(i);\n        if (!mergedKeys.contains(newKey)) {\n          mergedKeys.add(newKey);\n        }\n      }\n    }\n    return new DownloadRequest(\n        id, type, newRequest.uri, mergedKeys, newRequest.customCacheKey, newRequest.data);\n  }\n\n  @Override\n  public String toString() {\n    return type + \":\" + id;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (!(o instanceof DownloadRequest)) {\n      return false;\n    }\n    DownloadRequest that = (DownloadRequest) o;\n    return id.equals(that.id)\n        && type.equals(that.type)\n        && uri.equals(that.uri)\n        && streamKeys.equals(that.streamKeys)\n        && Util.areEqual(customCacheKey, that.customCacheKey)\n        && Arrays.equals(data, that.data);\n  }\n\n  @Override\n  public final int hashCode() {\n    int result = type.hashCode();\n    result = 31 * result + id.hashCode();\n    result = 31 * result + type.hashCode();\n    result = 31 * result + uri.hashCode();\n    result = 31 * result + streamKeys.hashCode();\n    result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);\n    result = 31 * result + Arrays.hashCode(data);\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(id);\n    dest.writeString(type);\n    dest.writeString(uri.toString());\n    dest.writeInt(streamKeys.size());\n    for (int i = 0; i < streamKeys.size(); i++) {\n      dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0);\n    }\n    dest.writeString(customCacheKey);\n    dest.writeInt(data.length);\n    dest.writeByteArray(data);\n  }\n\n  public static final Creator<DownloadRequest> CREATOR =\n      new Creator<DownloadRequest>() {\n\n        @Override\n        public DownloadRequest createFromParcel(Parcel in) {\n          return new DownloadRequest(in);\n        }\n\n        @Override\n        public DownloadRequest[] newArray(int size) {\n          return new DownloadRequest[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE;\n\nimport android.app.Notification;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport com.google.android.exoplayer2.scheduler.Requirements;\nimport com.google.android.exoplayer2.scheduler.Scheduler;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.NotificationUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.HashMap;\nimport java.util.List;\n\n/** A {@link Service} for downloading media. */\npublic abstract class DownloadService extends Service {\n\n  /**\n   * Starts a download service to resume any ongoing downloads. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_INIT =\n      \"com.google.android.exoplayer.downloadService.action.INIT\";\n\n  /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */\n  private static final String ACTION_RESTART =\n      \"com.google.android.exoplayer.downloadService.action.RESTART\";\n\n  /**\n   * Adds a new download. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_DOWNLOAD_REQUEST} - A {@link DownloadRequest} defining the download to be\n   *       added.\n   *   <li>{@link #KEY_STOP_REASON} - An initial stop reason for the download. If omitted {@link\n   *       Download#STOP_REASON_NONE} is used.\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_ADD_DOWNLOAD =\n      \"com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD\";\n\n  /**\n   * Removes a download. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_CONTENT_ID} - The content id of a download to remove.\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_REMOVE_DOWNLOAD =\n      \"com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD\";\n\n  /**\n   * Removes all downloads. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_REMOVE_ALL_DOWNLOADS =\n      \"com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS\";\n\n  /**\n   * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_RESUME_DOWNLOADS =\n      \"com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS\";\n\n  /**\n   * Pauses all downloads. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_PAUSE_DOWNLOADS =\n      \"com.google.android.exoplayer.downloadService.action.PAUSE_DOWNLOADS\";\n\n  /**\n   * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link\n   * Download#STOP_REASON_NONE}. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop\n   *       reason. If omitted, all downloads will be updated.\n   *   <li>{@link #KEY_STOP_REASON} - An application provided reason for stopping the download or\n   *       downloads, or {@link Download#STOP_REASON_NONE} to clear the stop reason.\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_SET_STOP_REASON =\n      \"com.google.android.exoplayer.downloadService.action.SET_STOP_REASON\";\n\n  /**\n   * Sets the requirements that need to be met for downloads to progress. Extras:\n   *\n   * <ul>\n   *   <li>{@link #KEY_REQUIREMENTS} - A {@link Requirements}.\n   *   <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.\n   * </ul>\n   */\n  public static final String ACTION_SET_REQUIREMENTS =\n      \"com.google.android.exoplayer.downloadService.action.SET_REQUIREMENTS\";\n\n  /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */\n  public static final String KEY_DOWNLOAD_REQUEST = \"download_request\";\n\n  /**\n   * Key for the {@link String} content id in {@link #ACTION_SET_STOP_REASON} and {@link\n   * #ACTION_REMOVE_DOWNLOAD} intents.\n   */\n  public static final String KEY_CONTENT_ID = \"content_id\";\n\n  /**\n   * Key for the integer stop reason in {@link #ACTION_SET_STOP_REASON} and {@link\n   * #ACTION_ADD_DOWNLOAD} intents.\n   */\n  public static final String KEY_STOP_REASON = \"stop_reason\";\n\n  /** Key for the {@link Requirements} in {@link #ACTION_SET_REQUIREMENTS} intents. */\n  public static final String KEY_REQUIREMENTS = \"requirements\";\n\n  /**\n   * Key for a boolean extra that can be set on any intent to indicate whether the service was\n   * started in the foreground. If set, the service is guaranteed to call {@link\n   * #startForeground(int, Notification)}.\n   */\n  public static final String KEY_FOREGROUND = \"foreground\";\n\n  /** Invalid foreground notification id that can be used to run the service in the background. */\n  public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0;\n\n  /** Default foreground notification update interval in milliseconds. */\n  public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000;\n\n  private static final String TAG = \"DownloadService\";\n\n  // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the\n  // process is running). This allows DownloadService to restart when there's no scheduler.\n  private static final HashMap<Class<? extends DownloadService>, DownloadManagerHelper>\n      downloadManagerListeners = new HashMap<>();\n\n  @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater;\n  @Nullable private final String channelId;\n  @StringRes private final int channelNameResourceId;\n  @StringRes private final int channelDescriptionResourceId;\n\n  @Nullable private DownloadManager downloadManager;\n  private int lastStartId;\n  private boolean startedInForeground;\n  private boolean taskRemoved;\n  private boolean isDestroyed;\n\n  /**\n   * Creates a DownloadService.\n   *\n   * <p>If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the\n   * service will only ever run in the background. No foreground notification will be displayed and\n   * {@link #getScheduler()} will not be called.\n   *\n   * <p>If {@code foregroundNotificationId} is not {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the\n   * service will run in the foreground. The foreground notification will be updated at least as\n   * often as the interval specified by {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.\n   *\n   * @param foregroundNotificationId The notification id for the foreground notification, or {@link\n   *     #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background.\n   */\n  protected DownloadService(int foregroundNotificationId) {\n    this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL);\n  }\n\n  /**\n   * Creates a DownloadService.\n   *\n   * @param foregroundNotificationId The notification id for the foreground notification, or {@link\n   *     #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background.\n   * @param foregroundNotificationUpdateInterval The maximum interval between updates to the\n   *     foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is\n   *     {@link #FOREGROUND_NOTIFICATION_ID_NONE}.\n   */\n  protected DownloadService(\n      int foregroundNotificationId, long foregroundNotificationUpdateInterval) {\n    this(\n        foregroundNotificationId,\n        foregroundNotificationUpdateInterval,\n        /* channelId= */ null,\n        /* channelNameResourceId= */ 0,\n        /* channelDescriptionResourceId= */ 0);\n  }\n\n  /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */\n  @Deprecated\n  protected DownloadService(\n      int foregroundNotificationId,\n      long foregroundNotificationUpdateInterval,\n      @Nullable String channelId,\n      @StringRes int channelNameResourceId) {\n    this(\n        foregroundNotificationId,\n        foregroundNotificationUpdateInterval,\n        channelId,\n        channelNameResourceId,\n        /* channelDescriptionResourceId= */ 0);\n  }\n\n  /**\n   * Creates a DownloadService.\n   *\n   * @param foregroundNotificationId The notification id for the foreground notification, or {@link\n   *     #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background.\n   * @param foregroundNotificationUpdateInterval The maximum interval between updates to the\n   *     foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is\n   *     {@link #FOREGROUND_NOTIFICATION_ID_NONE}.\n   * @param channelId An id for a low priority notification channel to create, or {@code null} if\n   *     the app will take care of creating a notification channel if needed. If specified, must be\n   *     unique per package. The value may be truncated if it's too long. Ignored if {@code\n   *     foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.\n   * @param channelNameResourceId A string resource identifier for the user visible name of the\n   *     notification channel. The recommended maximum length is 40 characters. The value may be\n   *     truncated if it's too long. Ignored if {@code channelId} is null or if {@code\n   *     foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.\n   * @param channelDescriptionResourceId A string resource identifier for the user visible\n   *     description of the notification channel, or 0 if no description is provided. The\n   *     recommended maximum length is 300 characters. The value may be truncated if it is too long.\n   *     Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link\n   *     #FOREGROUND_NOTIFICATION_ID_NONE}.\n   */\n  protected DownloadService(\n      int foregroundNotificationId,\n      long foregroundNotificationUpdateInterval,\n      @Nullable String channelId,\n      @StringRes int channelNameResourceId,\n      @StringRes int channelDescriptionResourceId) {\n    if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) {\n      this.foregroundNotificationUpdater = null;\n      this.channelId = null;\n      this.channelNameResourceId = 0;\n      this.channelDescriptionResourceId = 0;\n    } else {\n      this.foregroundNotificationUpdater =\n          new ForegroundNotificationUpdater(\n              foregroundNotificationId, foregroundNotificationUpdateInterval);\n      this.channelId = channelId;\n      this.channelNameResourceId = channelNameResourceId;\n      this.channelDescriptionResourceId = channelDescriptionResourceId;\n    }\n  }\n\n  /**\n   * Builds an {@link Intent} for adding a new download.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param downloadRequest The request to be executed.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildAddDownloadIntent(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      DownloadRequest downloadRequest,\n      boolean foreground) {\n    return buildAddDownloadIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground);\n  }\n\n  /**\n   * Builds an {@link Intent} for adding a new download.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param downloadRequest The request to be executed.\n   * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE}\n   *     if the download should be started.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildAddDownloadIntent(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      DownloadRequest downloadRequest,\n      int stopReason,\n      boolean foreground) {\n    return getIntent(context, clazz, ACTION_ADD_DOWNLOAD, foreground)\n        .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest)\n        .putExtra(KEY_STOP_REASON, stopReason);\n  }\n\n  /**\n   * Builds an {@link Intent} for removing the download with the {@code id}.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param id The content id.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildRemoveDownloadIntent(\n      Context context, Class<? extends DownloadService> clazz, String id, boolean foreground) {\n    return getIntent(context, clazz, ACTION_REMOVE_DOWNLOAD, foreground)\n        .putExtra(KEY_CONTENT_ID, id);\n  }\n\n  /**\n   * Builds an {@link Intent} for removing all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildRemoveAllDownloadsIntent(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground);\n  }\n\n  /**\n   * Builds an {@link Intent} for resuming all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildResumeDownloadsIntent(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    return getIntent(context, clazz, ACTION_RESUME_DOWNLOADS, foreground);\n  }\n\n  /**\n   * Builds an {@link Intent} to pause all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildPauseDownloadsIntent(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    return getIntent(context, clazz, ACTION_PAUSE_DOWNLOADS, foreground);\n  }\n\n  /**\n   * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the\n   * stop reason, pass {@link Download#STOP_REASON_NONE}.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param id The content id, or {@code null} to set the stop reason for all downloads.\n   * @param stopReason An application defined stop reason.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildSetStopReasonIntent(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      @Nullable String id,\n      int stopReason,\n      boolean foreground) {\n    return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground)\n        .putExtra(KEY_CONTENT_ID, id)\n        .putExtra(KEY_STOP_REASON, stopReason);\n  }\n\n  /**\n   * Builds an {@link Intent} for setting the requirements that need to be met for downloads to\n   * progress.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service being targeted by the intent.\n   * @param requirements A {@link Requirements}.\n   * @param foreground Whether this intent will be used to start the service in the foreground.\n   * @return The created intent.\n   */\n  public static Intent buildSetRequirementsIntent(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      Requirements requirements,\n      boolean foreground) {\n    return getIntent(context, clazz, ACTION_SET_REQUIREMENTS, foreground)\n        .putExtra(KEY_REQUIREMENTS, requirements);\n  }\n\n  /**\n   * Starts the service if not started already and adds a new download.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param downloadRequest The request to be executed.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendAddDownload(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      DownloadRequest downloadRequest,\n      boolean foreground) {\n    Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and adds a new download.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param downloadRequest The request to be executed.\n   * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE}\n   *     if the download should be started.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendAddDownload(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      DownloadRequest downloadRequest,\n      int stopReason,\n      boolean foreground) {\n    Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, stopReason, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and removes a download.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param id The content id.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendRemoveDownload(\n      Context context, Class<? extends DownloadService> clazz, String id, boolean foreground) {\n    Intent intent = buildRemoveDownloadIntent(context, clazz, id, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and removes all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendRemoveAllDownloads(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and resumes all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendResumeDownloads(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    Intent intent = buildResumeDownloadsIntent(context, clazz, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and pauses all downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendPauseDownloads(\n      Context context, Class<? extends DownloadService> clazz, boolean foreground) {\n    Intent intent = buildPauseDownloadsIntent(context, clazz, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and sets the stop reason for one or all downloads. To\n   * clear stop reason, pass {@link Download#STOP_REASON_NONE}.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param id The content id, or {@code null} to set the stop reason for all downloads.\n   * @param stopReason An application defined stop reason.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendSetStopReason(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      @Nullable String id,\n      int stopReason,\n      boolean foreground) {\n    Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts the service if not started already and sets the requirements that need to be met for\n   * downloads to progress.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @param requirements A {@link Requirements}.\n   * @param foreground Whether the service is started in the foreground.\n   */\n  public static void sendSetRequirements(\n      Context context,\n      Class<? extends DownloadService> clazz,\n      Requirements requirements,\n      boolean foreground) {\n    Intent intent = buildSetRequirementsIntent(context, clazz, requirements, foreground);\n    startService(context, intent, foreground);\n  }\n\n  /**\n   * Starts a download service to resume any ongoing downloads.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @see #startForeground(Context, Class)\n   */\n  public static void start(Context context, Class<? extends DownloadService> clazz) {\n    context.startService(getIntent(context, clazz, ACTION_INIT));\n  }\n\n  /**\n   * Starts the service in the foreground without adding a new download request. If there are any\n   * not finished downloads and the requirements are met, the service resumes downloading. Otherwise\n   * it stops immediately.\n   *\n   * @param context A {@link Context}.\n   * @param clazz The concrete download service to be started.\n   * @see #start(Context, Class)\n   */\n  public static void startForeground(Context context, Class<? extends DownloadService> clazz) {\n    Intent intent = getIntent(context, clazz, ACTION_INIT, true);\n    Util.startForegroundService(context, intent);\n  }\n\n  @Override\n  public void onCreate() {\n    if (channelId != null) {\n      NotificationUtil.createNotificationChannel(\n          this,\n          channelId,\n          channelNameResourceId,\n          channelDescriptionResourceId,\n          NotificationUtil.IMPORTANCE_LOW);\n    }\n    Class<? extends DownloadService> clazz = getClass();\n    DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);\n    if (downloadManagerHelper == null) {\n      DownloadManager downloadManager = getDownloadManager();\n      downloadManager.resumeDownloads();\n      downloadManagerHelper =\n          new DownloadManagerHelper(\n              getApplicationContext(), downloadManager, getScheduler(), clazz);\n      downloadManagerListeners.put(clazz, downloadManagerHelper);\n    }\n    downloadManager = downloadManagerHelper.downloadManager;\n    downloadManagerHelper.attachService(this);\n  }\n\n  @Override\n  public int onStartCommand(Intent intent, int flags, int startId) {\n    lastStartId = startId;\n    taskRemoved = false;\n    @Nullable String intentAction = null;\n    @Nullable String contentId = null;\n    if (intent != null) {\n      intentAction = intent.getAction();\n      startedInForeground |=\n          intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);\n      contentId = intent.getStringExtra(KEY_CONTENT_ID);\n    }\n    // intentAction is null if the service is restarted or no action is specified.\n    if (intentAction == null) {\n      intentAction = ACTION_INIT;\n    }\n    DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager);\n    switch (intentAction) {\n      case ACTION_INIT:\n      case ACTION_RESTART:\n        // Do nothing.\n        break;\n      case ACTION_ADD_DOWNLOAD:\n        @Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST);\n        if (downloadRequest == null) {\n          Log.e(TAG, \"Ignored ADD_DOWNLOAD: Missing \" + KEY_DOWNLOAD_REQUEST + \" extra\");\n        } else {\n          int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE);\n          downloadManager.addDownload(downloadRequest, stopReason);\n        }\n        break;\n      case ACTION_REMOVE_DOWNLOAD:\n        if (contentId == null) {\n          Log.e(TAG, \"Ignored REMOVE_DOWNLOAD: Missing \" + KEY_CONTENT_ID + \" extra\");\n        } else {\n          downloadManager.removeDownload(contentId);\n        }\n        break;\n      case ACTION_REMOVE_ALL_DOWNLOADS:\n        downloadManager.removeAllDownloads();\n        break;\n      case ACTION_RESUME_DOWNLOADS:\n        downloadManager.resumeDownloads();\n        break;\n      case ACTION_PAUSE_DOWNLOADS:\n        downloadManager.pauseDownloads();\n        break;\n      case ACTION_SET_STOP_REASON:\n        if (!intent.hasExtra(KEY_STOP_REASON)) {\n          Log.e(TAG, \"Ignored SET_STOP_REASON: Missing \" + KEY_STOP_REASON + \" extra\");\n        } else {\n          int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0);\n          downloadManager.setStopReason(contentId, stopReason);\n        }\n        break;\n      case ACTION_SET_REQUIREMENTS:\n        @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS);\n        if (requirements == null) {\n          Log.e(TAG, \"Ignored SET_REQUIREMENTS: Missing \" + KEY_REQUIREMENTS + \" extra\");\n        } else {\n          downloadManager.setRequirements(requirements);\n        }\n        break;\n      default:\n        Log.e(TAG, \"Ignored unrecognized action: \" + intentAction);\n        break;\n    }\n\n    if (downloadManager.isIdle()) {\n      stop();\n    }\n    return START_STICKY;\n  }\n\n  @Override\n  public void onTaskRemoved(Intent rootIntent) {\n    taskRemoved = true;\n  }\n\n  @Override\n  public void onDestroy() {\n    isDestroyed = true;\n    DownloadManagerHelper downloadManagerHelper =\n        Assertions.checkNotNull(downloadManagerListeners.get(getClass()));\n    boolean unschedule = !downloadManagerHelper.downloadManager.isWaitingForRequirements();\n    downloadManagerHelper.detachService(this, unschedule);\n    if (foregroundNotificationUpdater != null) {\n      foregroundNotificationUpdater.stopPeriodicUpdates();\n    }\n  }\n\n  /**\n   * Throws {@link UnsupportedOperationException} because this service is not designed to be bound.\n   */\n  @Nullable\n  @Override\n  public final IBinder onBind(Intent intent) {\n    throw new UnsupportedOperationException();\n  }\n\n  /**\n   * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the\n   * life cycle of the process.\n   */\n  protected abstract DownloadManager getDownloadManager();\n\n  /**\n   * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take\n   * place are met. If {@code null}, the service will only be restarted if the process is still in\n   * memory when the requirements are met.\n   */\n  protected abstract @Nullable Scheduler getScheduler();\n\n  /**\n   * Returns a notification to be displayed when this service running in the foreground. This method\n   * is called when there is a download state change and periodically while there are active\n   * downloads. The periodic update interval can be set using {@link #DownloadService(int, long)}.\n   *\n   * <p>On API level 26 and above, this method may also be called just before the service stops,\n   * with an empty {@code downloads} array. The returned notification is used to satisfy system\n   * requirements for foreground services.\n   *\n   * <p>Download services that do not wish to run in the foreground should be created by setting the\n   * {@code foregroundNotificationId} constructor argument to {@link\n   * #FOREGROUND_NOTIFICATION_ID_NONE}. This method will not be called in this case, meaning it can\n   * be implemented to throw {@link UnsupportedOperationException}.\n   *\n   * @param downloads The current downloads.\n   * @return The foreground notification to display.\n   */\n  protected abstract Notification getForegroundNotification(List<Download> downloads);\n\n  /**\n   * Invalidates the current foreground notification and causes {@link\n   * #getForegroundNotification(List)} to be invoked again if the service isn't stopped.\n   */\n  protected final void invalidateForegroundNotification() {\n    if (foregroundNotificationUpdater != null && !isDestroyed) {\n      foregroundNotificationUpdater.invalidate();\n    }\n  }\n\n  /**\n   * Called when the state of a download changes. The default implementation is a no-op.\n   *\n   * @param download The new state of the download.\n   */\n  protected void onDownloadChanged(Download download) {\n    // Do nothing.\n  }\n\n  /**\n   * Called when a download is removed. The default implementation is a no-op.\n   *\n   * @param download The last state of the download before it was removed.\n   */\n  protected void onDownloadRemoved(Download download) {\n    // Do nothing.\n  }\n\n  private void notifyDownloadChanged(Download download) {\n    onDownloadChanged(download);\n    if (foregroundNotificationUpdater != null) {\n      if (download.state == Download.STATE_DOWNLOADING\n          || download.state == Download.STATE_REMOVING\n          || download.state == Download.STATE_RESTARTING) {\n        foregroundNotificationUpdater.startPeriodicUpdates();\n      } else {\n        foregroundNotificationUpdater.invalidate();\n      }\n    }\n  }\n\n  private void notifyDownloadRemoved(Download download) {\n    onDownloadRemoved(download);\n    if (foregroundNotificationUpdater != null) {\n      foregroundNotificationUpdater.invalidate();\n    }\n  }\n\n  private void stop() {\n    if (foregroundNotificationUpdater != null) {\n      foregroundNotificationUpdater.stopPeriodicUpdates();\n      // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].\n      if (startedInForeground && Util.SDK_INT >= 26) {\n        foregroundNotificationUpdater.showNotificationIfNotAlready();\n      }\n    }\n    if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].\n      stopSelf();\n    } else {\n      stopSelfResult(lastStartId);\n    }\n  }\n\n  private static Intent getIntent(\n      Context context, Class<? extends DownloadService> clazz, String action, boolean foreground) {\n    return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground);\n  }\n\n  private static Intent getIntent(\n      Context context, Class<? extends DownloadService> clazz, String action) {\n    return new Intent(context, clazz).setAction(action);\n  }\n\n  private static void startService(Context context, Intent intent, boolean foreground) {\n    if (foreground) {\n      Util.startForegroundService(context, intent);\n    } else {\n      context.startService(intent);\n    }\n  }\n\n  private final class ForegroundNotificationUpdater {\n\n    private final int notificationId;\n    private final long updateInterval;\n    private final Handler handler;\n\n    private boolean periodicUpdatesStarted;\n    private boolean notificationDisplayed;\n\n    public ForegroundNotificationUpdater(int notificationId, long updateInterval) {\n      this.notificationId = notificationId;\n      this.updateInterval = updateInterval;\n      this.handler = new Handler(Looper.getMainLooper());\n    }\n\n    public void startPeriodicUpdates() {\n      periodicUpdatesStarted = true;\n      update();\n    }\n\n    public void stopPeriodicUpdates() {\n      periodicUpdatesStarted = false;\n      handler.removeCallbacksAndMessages(null);\n    }\n\n    public void showNotificationIfNotAlready() {\n      if (!notificationDisplayed) {\n        update();\n      }\n    }\n\n    public void invalidate() {\n      if (notificationDisplayed) {\n        update();\n      }\n    }\n\n    private void update() {\n      List<Download> downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads();\n      startForeground(notificationId, getForegroundNotification(downloads));\n      notificationDisplayed = true;\n      if (periodicUpdatesStarted) {\n        handler.removeCallbacksAndMessages(null);\n        handler.postDelayed(this::update, updateInterval);\n      }\n    }\n  }\n\n  private static final class DownloadManagerHelper implements DownloadManager.Listener {\n\n    private final Context context;\n    private final DownloadManager downloadManager;\n    @Nullable private final Scheduler scheduler;\n    private final Class<? extends DownloadService> serviceClass;\n    @Nullable private DownloadService downloadService;\n\n    private DownloadManagerHelper(\n        Context context,\n        DownloadManager downloadManager,\n        @Nullable Scheduler scheduler,\n        Class<? extends DownloadService> serviceClass) {\n      this.context = context;\n      this.downloadManager = downloadManager;\n      this.scheduler = scheduler;\n      this.serviceClass = serviceClass;\n      downloadManager.addListener(this);\n      if (scheduler != null) {\n        Requirements requirements = downloadManager.getRequirements();\n        setSchedulerEnabled(\n            scheduler, /* enabled= */ !requirements.checkRequirements(context), requirements);\n      }\n    }\n\n    public void attachService(DownloadService downloadService) {\n      Assertions.checkState(this.downloadService == null);\n      this.downloadService = downloadService;\n    }\n\n    public void detachService(DownloadService downloadService, boolean unschedule) {\n      Assertions.checkState(this.downloadService == downloadService);\n      this.downloadService = null;\n      if (scheduler != null && unschedule) {\n        scheduler.cancel();\n      }\n    }\n\n    @Override\n    public void onDownloadChanged(DownloadManager downloadManager, Download download) {\n      if (downloadService != null) {\n        downloadService.notifyDownloadChanged(download);\n      }\n    }\n\n    @Override\n    public void onDownloadRemoved(DownloadManager downloadManager, Download download) {\n      if (downloadService != null) {\n        downloadService.notifyDownloadRemoved(download);\n      }\n    }\n\n    @Override\n    public final void onIdle(DownloadManager downloadManager) {\n      if (downloadService != null) {\n        downloadService.stop();\n      }\n    }\n\n    @Override\n    public void onRequirementsStateChanged(\n        DownloadManager downloadManager,\n        Requirements requirements,\n        @Requirements.RequirementFlags int notMetRequirements) {\n      boolean requirementsMet = notMetRequirements == 0;\n      if (downloadService == null && requirementsMet) {\n        try {\n          Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);\n          context.startService(intent);\n        } catch (IllegalStateException e) {\n          /* startService fails if the app is in the background then don't stop the scheduler. */\n          return;\n        }\n      }\n      if (scheduler != null) {\n        setSchedulerEnabled(scheduler, /* enabled= */ !requirementsMet, requirements);\n      }\n    }\n\n    private void setSchedulerEnabled(\n        Scheduler scheduler, boolean enabled, Requirements requirements) {\n      if (!enabled) {\n        scheduler.cancel();\n      } else {\n        String servicePackage = context.getPackageName();\n        boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);\n        if (!success) {\n          Log.e(TAG, \"Scheduling downloads failed.\");\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/Downloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\n\n/** Downloads and removes a piece of content. */\npublic interface Downloader {\n\n  /** Receives progress updates during download operations. */\n  interface ProgressListener {\n\n    /**\n     * Called when progress is made during a download operation.\n     *\n     * @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if\n     *     unknown.\n     * @param bytesDownloaded The number of bytes that have been downloaded.\n     * @param percentDownloaded The percentage of the content that has been downloaded, or {@link\n     *     C#PERCENTAGE_UNSET}.\n     */\n    void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded);\n  }\n\n  /**\n   * Downloads the content.\n   *\n   * @param progressListener A listener to receive progress updates, or {@code null}.\n   * @throws DownloadException Thrown if the content cannot be downloaded.\n   * @throws InterruptedException If the thread has been interrupted.\n   * @throws IOException Thrown when there is an io error while downloading.\n   */\n  void download(@Nullable ProgressListener progressListener)\n      throws InterruptedException, IOException;\n\n  /** Cancels the download operation and prevents future download operations from running. */\n  void cancel();\n\n  /**\n   * Removes the content.\n   *\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   */\n  void remove() throws InterruptedException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.upstream.PriorityDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSink;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSinkFactory;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;\nimport com.google.android.exoplayer2.upstream.cache.CacheUtil;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\n\n/** A helper class that holds necessary parameters for {@link Downloader} construction. */\npublic final class DownloaderConstructorHelper {\n\n  private final Cache cache;\n  @Nullable private final CacheKeyFactory cacheKeyFactory;\n  @Nullable private final PriorityTaskManager priorityTaskManager;\n  private final CacheDataSourceFactory onlineCacheDataSourceFactory;\n  private final CacheDataSourceFactory offlineCacheDataSourceFactory;\n\n  /**\n   * @param cache Cache instance to be used to store downloaded data.\n   * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for\n   *     downloading data.\n   */\n  public DownloaderConstructorHelper(Cache cache, DataSource.Factory upstreamFactory) {\n    this(\n        cache,\n        upstreamFactory,\n        /* cacheReadDataSourceFactory= */ null,\n        /* cacheWriteDataSinkFactory= */ null,\n        /* priorityTaskManager= */ null);\n  }\n\n  /**\n   * @param cache Cache instance to be used to store downloaded data.\n   * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for\n   *     downloading data.\n   * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s\n   *     for reading data from the cache. If null then a {@link FileDataSource.Factory} will be\n   *     used.\n   * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s\n   *     for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used.\n   * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null,\n   *     downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst\n   *     downloading.\n   */\n  public DownloaderConstructorHelper(\n      Cache cache,\n      DataSource.Factory upstreamFactory,\n      @Nullable DataSource.Factory cacheReadDataSourceFactory,\n      @Nullable DataSink.Factory cacheWriteDataSinkFactory,\n      @Nullable PriorityTaskManager priorityTaskManager) {\n    this(\n        cache,\n        upstreamFactory,\n        cacheReadDataSourceFactory,\n        cacheWriteDataSinkFactory,\n        priorityTaskManager,\n        /* cacheKeyFactory= */ null);\n  }\n\n  /**\n   * @param cache Cache instance to be used to store downloaded data.\n   * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for\n   *     downloading data.\n   * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s\n   *     for reading data from the cache. If null then a {@link FileDataSource.Factory} will be\n   *     used.\n   * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s\n   *     for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used.\n   * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null,\n   *     downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst\n   *     downloading.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   */\n  public DownloaderConstructorHelper(\n      Cache cache,\n      DataSource.Factory upstreamFactory,\n      @Nullable DataSource.Factory cacheReadDataSourceFactory,\n      @Nullable DataSink.Factory cacheWriteDataSinkFactory,\n      @Nullable PriorityTaskManager priorityTaskManager,\n      @Nullable CacheKeyFactory cacheKeyFactory) {\n    if (priorityTaskManager != null) {\n      upstreamFactory =\n          new PriorityDataSourceFactory(upstreamFactory, priorityTaskManager, C.PRIORITY_DOWNLOAD);\n    }\n    DataSource.Factory readDataSourceFactory =\n        cacheReadDataSourceFactory != null\n            ? cacheReadDataSourceFactory\n            : new FileDataSource.Factory();\n    if (cacheWriteDataSinkFactory == null) {\n      cacheWriteDataSinkFactory =\n          new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);\n    }\n    onlineCacheDataSourceFactory =\n        new CacheDataSourceFactory(\n            cache,\n            upstreamFactory,\n            readDataSourceFactory,\n            cacheWriteDataSinkFactory,\n            CacheDataSource.FLAG_BLOCK_ON_CACHE,\n            /* eventListener= */ null,\n            cacheKeyFactory);\n    offlineCacheDataSourceFactory =\n        new CacheDataSourceFactory(\n            cache,\n            DummyDataSource.FACTORY,\n            readDataSourceFactory,\n            null,\n            CacheDataSource.FLAG_BLOCK_ON_CACHE,\n            /* eventListener= */ null,\n            cacheKeyFactory);\n    this.cache = cache;\n    this.priorityTaskManager = priorityTaskManager;\n    this.cacheKeyFactory = cacheKeyFactory;\n  }\n\n  /** Returns the {@link Cache} instance. */\n  public Cache getCache() {\n    return cache;\n  }\n\n  /** Returns the {@link CacheKeyFactory}. */\n  public CacheKeyFactory getCacheKeyFactory() {\n    return cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY;\n  }\n\n  /** Returns a {@link PriorityTaskManager} instance. */\n  public PriorityTaskManager getPriorityTaskManager() {\n    // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager\n    // each time so clients don't affect each other over the dummy PriorityTaskManager instance.\n    return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager();\n  }\n\n  /** Returns a new {@link CacheDataSource} instance. */\n  public CacheDataSource createCacheDataSource() {\n    return onlineCacheDataSourceFactory.createDataSource();\n  }\n\n  /**\n   * Returns a new {@link CacheDataSource} instance which accesses cache read-only and throws an\n   * exception on cache miss.\n   */\n  public CacheDataSource createOfflineCacheDataSource() {\n    return offlineCacheDataSourceFactory.createDataSource();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/DownloaderFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\n/** Creates {@link Downloader Downloaders} for given {@link DownloadRequest DownloadRequests}. */\npublic interface DownloaderFactory {\n\n  /**\n   * Creates a {@link Downloader} to perform the given {@link DownloadRequest}.\n   *\n   * @param action The action.\n   * @return The downloader.\n   */\n  Downloader createDownloader(DownloadRequest action);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport java.util.List;\n\n/**\n * A manifest that can generate copies of itself including only the streams specified by the given\n * keys.\n *\n * @param <T> The manifest type.\n */\npublic interface FilterableManifest<T> {\n\n  /**\n   * Returns a copy of the manifest including only the streams specified by the given keys. If the\n   * manifest is unchanged then the instance may return itself.\n   *\n   * @param streamKeys A non-empty list of stream keys.\n   * @return The filtered manifest.\n   */\n  T copy(List<StreamKey> streamKeys);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\n\n/**\n * A manifest parser that includes only the streams identified by the given stream keys.\n *\n * @param <T> The {@link FilterableManifest} type.\n */\npublic final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> {\n\n  private final Parser<? extends T> parser;\n  @Nullable private final List<StreamKey> streamKeys;\n\n  /**\n   * @param parser A parser for the manifest that will be filtered.\n   * @param streamKeys The stream keys. If null or empty then filtering will not occur.\n   */\n  public FilteringManifestParser(Parser<? extends T> parser, @Nullable List<StreamKey> streamKeys) {\n    this.parser = parser;\n    this.streamKeys = streamKeys;\n  }\n\n  @Override\n  public T parse(Uri uri, InputStream inputStream) throws IOException {\n    T manifest = parser.parse(uri, inputStream);\n    return streamKeys == null || streamKeys.isEmpty() ? manifest : manifest.copy(streamKeys);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;\nimport com.google.android.exoplayer2.upstream.cache.CacheUtil;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport java.io.IOException;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * A downloader for progressive media streams.\n *\n * <p>The downloader attempts to download the entire media bytes referenced by a {@link Uri} into a\n * cache as defined by {@link DownloaderConstructorHelper}. Callers can use the constructor to\n * specify a custom cache key for the downloaded bytes.\n *\n * <p>The downloader will avoid downloading already-downloaded media bytes.\n */\npublic final class ProgressiveDownloader implements Downloader {\n\n  private static final int BUFFER_SIZE_BYTES = 128 * 1024;\n\n  private final DataSpec dataSpec;\n  private final Cache cache;\n  private final CacheDataSource dataSource;\n  private final CacheKeyFactory cacheKeyFactory;\n  private final PriorityTaskManager priorityTaskManager;\n  private final AtomicBoolean isCanceled;\n\n  /**\n   * @param uri Uri of the data to be downloaded.\n   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache\n   *     indexing. May be null.\n   * @param constructorHelper A {@link DownloaderConstructorHelper} instance.\n   */\n  public ProgressiveDownloader(\n      Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {\n    this.dataSpec =\n        new DataSpec(\n            uri,\n            /* absoluteStreamPosition= */ 0,\n            C.LENGTH_UNSET,\n            customCacheKey,\n            /* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);\n    this.cache = constructorHelper.getCache();\n    this.dataSource = constructorHelper.createCacheDataSource();\n    this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();\n    this.priorityTaskManager = constructorHelper.getPriorityTaskManager();\n    isCanceled = new AtomicBoolean();\n  }\n\n  @Override\n  public void download(@Nullable ProgressListener progressListener)\n      throws InterruptedException, IOException {\n    priorityTaskManager.add(C.PRIORITY_DOWNLOAD);\n    try {\n      CacheUtil.cache(\n          dataSpec,\n          cache,\n          cacheKeyFactory,\n          dataSource,\n          new byte[BUFFER_SIZE_BYTES],\n          priorityTaskManager,\n          C.PRIORITY_DOWNLOAD,\n          progressListener == null ? null : new ProgressForwarder(progressListener),\n          isCanceled,\n          /* enableEOFException= */ true);\n    } finally {\n      priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);\n    }\n  }\n\n  @Override\n  public void cancel() {\n    isCanceled.set(true);\n  }\n\n  @Override\n  public void remove() {\n    CacheUtil.remove(dataSpec, cache, cacheKeyFactory);\n  }\n\n  private static final class ProgressForwarder implements CacheUtil.ProgressListener {\n\n    private final ProgressListener progessListener;\n\n    public ProgressForwarder(ProgressListener progressListener) {\n      this.progessListener = progressListener;\n    }\n\n    @Override\n    public void onProgress(long contentLength, long bytesCached, long newBytesCached) {\n      float percentDownloaded =\n          contentLength == C.LENGTH_UNSET || contentLength == 0\n              ? C.PERCENTAGE_UNSET\n              : ((bytesCached * 100f) / contentLength);\n      progessListener.onProgress(contentLength, bytesCached, percentDownloaded);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.net.Uri;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;\nimport com.google.android.exoplayer2.upstream.cache.CacheUtil;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * Base class for multi segment stream downloaders.\n *\n * @param <M> The type of the manifest object.\n */\npublic abstract class SegmentDownloader<M extends FilterableManifest<M>> implements Downloader {\n\n  /** Smallest unit of content to be downloaded. */\n  protected static class Segment implements Comparable<Segment> {\n\n    /** The start time of the segment in microseconds. */\n    public final long startTimeUs;\n\n    /** The {@link DataSpec} of the segment. */\n    public final DataSpec dataSpec;\n\n    /** Constructs a Segment. */\n    public Segment(long startTimeUs, DataSpec dataSpec) {\n      this.startTimeUs = startTimeUs;\n      this.dataSpec = dataSpec;\n    }\n\n    @Override\n    public int compareTo(Segment other) {\n      return Util.compareLong(startTimeUs, other.startTimeUs);\n    }\n  }\n\n  private static final int BUFFER_SIZE_BYTES = 128 * 1024;\n\n  private final DataSpec manifestDataSpec;\n  private final Cache cache;\n  private final CacheDataSource dataSource;\n  private final CacheDataSource offlineDataSource;\n  private final CacheKeyFactory cacheKeyFactory;\n  private final PriorityTaskManager priorityTaskManager;\n  private final ArrayList<StreamKey> streamKeys;\n  private final AtomicBoolean isCanceled;\n\n  /**\n   * @param manifestUri The {@link Uri} of the manifest to be downloaded.\n   * @param streamKeys Keys defining which streams in the manifest should be selected for download.\n   *     If empty, all streams are downloaded.\n   * @param constructorHelper A {@link DownloaderConstructorHelper} instance.\n   */\n  public SegmentDownloader(\n      Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {\n    this.manifestDataSpec = getCompressibleDataSpec(manifestUri);\n    this.streamKeys = new ArrayList<>(streamKeys);\n    this.cache = constructorHelper.getCache();\n    this.dataSource = constructorHelper.createCacheDataSource();\n    this.offlineDataSource = constructorHelper.createOfflineCacheDataSource();\n    this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();\n    this.priorityTaskManager = constructorHelper.getPriorityTaskManager();\n    isCanceled = new AtomicBoolean();\n  }\n\n  /**\n   * Downloads the selected streams in the media. If multiple streams are selected, they are\n   * downloaded in sync with one another.\n   *\n   * @throws IOException Thrown when there is an error downloading.\n   * @throws InterruptedException If the thread has been interrupted.\n   */\n  @Override\n  public final void download(@Nullable ProgressListener progressListener)\n      throws IOException, InterruptedException {\n    priorityTaskManager.add(C.PRIORITY_DOWNLOAD);\n    try {\n      // Get the manifest and all of the segments.\n      M manifest = getManifest(dataSource, manifestDataSpec);\n      if (!streamKeys.isEmpty()) {\n        manifest = manifest.copy(streamKeys);\n      }\n      List<Segment> segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false);\n\n      // Scan the segments, removing any that are fully downloaded.\n      int totalSegments = segments.size();\n      int segmentsDownloaded = 0;\n      long contentLength = 0;\n      long bytesDownloaded = 0;\n      for (int i = segments.size() - 1; i >= 0; i--) {\n        Segment segment = segments.get(i);\n        Pair<Long, Long> segmentLengthAndBytesDownloaded =\n            CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory);\n        long segmentLength = segmentLengthAndBytesDownloaded.first;\n        long segmentBytesDownloaded = segmentLengthAndBytesDownloaded.second;\n        bytesDownloaded += segmentBytesDownloaded;\n        if (segmentLength != C.LENGTH_UNSET) {\n          if (segmentLength == segmentBytesDownloaded) {\n            // The segment is fully downloaded.\n            segmentsDownloaded++;\n            segments.remove(i);\n          }\n          if (contentLength != C.LENGTH_UNSET) {\n            contentLength += segmentLength;\n          }\n        } else {\n          contentLength = C.LENGTH_UNSET;\n        }\n      }\n      Collections.sort(segments);\n\n      // Download the segments.\n      @Nullable ProgressNotifier progressNotifier = null;\n      if (progressListener != null) {\n        progressNotifier =\n            new ProgressNotifier(\n                progressListener,\n                contentLength,\n                totalSegments,\n                bytesDownloaded,\n                segmentsDownloaded);\n      }\n      byte[] buffer = new byte[BUFFER_SIZE_BYTES];\n      for (int i = 0; i < segments.size(); i++) {\n        CacheUtil.cache(\n            segments.get(i).dataSpec,\n            cache,\n            cacheKeyFactory,\n            dataSource,\n            buffer,\n            priorityTaskManager,\n            C.PRIORITY_DOWNLOAD,\n            progressNotifier,\n            isCanceled,\n            true);\n        if (progressNotifier != null) {\n          progressNotifier.onSegmentDownloaded();\n        }\n      }\n    } finally {\n      priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);\n    }\n  }\n\n  @Override\n  public void cancel() {\n    isCanceled.set(true);\n  }\n\n  @Override\n  public final void remove() throws InterruptedException {\n    try {\n      M manifest = getManifest(offlineDataSource, manifestDataSpec);\n      List<Segment> segments = getSegments(offlineDataSource, manifest, true);\n      for (int i = 0; i < segments.size(); i++) {\n        removeDataSpec(segments.get(i).dataSpec);\n      }\n    } catch (IOException e) {\n      // Ignore exceptions when removing.\n    } finally {\n      // Always attempt to remove the manifest.\n      removeDataSpec(manifestDataSpec);\n    }\n  }\n\n  // Internal methods.\n\n  /**\n   * Loads and parses the manifest.\n   *\n   * @param dataSource The {@link DataSource} through which to load.\n   * @param dataSpec The manifest {@link DataSpec}.\n   * @return The manifest.\n   * @throws IOException If an error occurs reading data.\n   */\n  protected abstract M getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException;\n\n  /**\n   * Returns a list of all downloadable {@link Segment}s for a given manifest.\n   *\n   * @param dataSource The {@link DataSource} through which to load any required data.\n   * @param manifest The manifest containing the segments.\n   * @param allowIncompleteList Whether to continue in the case that a load error prevents all\n   *     segments from being listed. If true then a partial segment list will be returned. If false\n   *     an {@link IOException} will be thrown.\n   * @return The list of downloadable {@link Segment}s.\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if\n   *     the media is not in a form that allows for its segments to be listed.\n   */\n  protected abstract List<Segment> getSegments(\n      DataSource dataSource, M manifest, boolean allowIncompleteList)\n      throws InterruptedException, IOException;\n\n  private void removeDataSpec(DataSpec dataSpec) {\n    CacheUtil.remove(dataSpec, cache, cacheKeyFactory);\n  }\n\n  protected static DataSpec getCompressibleDataSpec(Uri uri) {\n    return new DataSpec(\n        uri,\n        /* absoluteStreamPosition= */ 0,\n        /* length= */ C.LENGTH_UNSET,\n        /* key= */ null,\n        /* flags= */ DataSpec.FLAG_ALLOW_GZIP);\n  }\n\n  private static final class ProgressNotifier implements CacheUtil.ProgressListener {\n\n    private final ProgressListener progressListener;\n\n    private final long contentLength;\n    private final int totalSegments;\n\n    private long bytesDownloaded;\n    private int segmentsDownloaded;\n\n    public ProgressNotifier(\n        ProgressListener progressListener,\n        long contentLength,\n        int totalSegments,\n        long bytesDownloaded,\n        int segmentsDownloaded) {\n      this.progressListener = progressListener;\n      this.contentLength = contentLength;\n      this.totalSegments = totalSegments;\n      this.bytesDownloaded = bytesDownloaded;\n      this.segmentsDownloaded = segmentsDownloaded;\n    }\n\n    @Override\n    public void onProgress(long requestLength, long bytesCached, long newBytesCached) {\n      bytesDownloaded += newBytesCached;\n      progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded());\n    }\n\n    public void onSegmentDownloaded() {\n      segmentsDownloaded++;\n      progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded());\n    }\n\n    private float getPercentDownloaded() {\n      if (contentLength != C.LENGTH_UNSET && contentLength != 0) {\n        return (bytesDownloaded * 100f) / contentLength;\n      } else if (totalSegments != 0) {\n        return (segmentsDownloaded * 100f) / totalSegments;\n      } else {\n        return C.PERCENTAGE_UNSET;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\n\n/**\n * A key for a subset of media which can be separately loaded (a \"stream\").\n *\n * <p>The stream key consists of a period index, a group index within the period and a track index\n * within the group. The interpretation of these indices depends on the type of media for which the\n * stream key is used.\n */\npublic final class StreamKey implements Comparable<StreamKey>, Parcelable {\n\n  /** The period index. */\n  public final int periodIndex;\n  /** The group index. */\n  public final int groupIndex;\n  /** The track index. */\n  public final int trackIndex;\n\n  /**\n   * @param groupIndex The group index.\n   * @param trackIndex The track index.\n   */\n  public StreamKey(int groupIndex, int trackIndex) {\n    this(0, groupIndex, trackIndex);\n  }\n\n  /**\n   * @param periodIndex The period index.\n   * @param groupIndex The group index.\n   * @param trackIndex The track index.\n   */\n  public StreamKey(int periodIndex, int groupIndex, int trackIndex) {\n    this.periodIndex = periodIndex;\n    this.groupIndex = groupIndex;\n    this.trackIndex = trackIndex;\n  }\n\n  /* package */ StreamKey(Parcel in) {\n    periodIndex = in.readInt();\n    groupIndex = in.readInt();\n    trackIndex = in.readInt();\n  }\n\n  @Override\n  public String toString() {\n    return periodIndex + \".\" + groupIndex + \".\" + trackIndex;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n\n    StreamKey that = (StreamKey) o;\n    return periodIndex == that.periodIndex\n        && groupIndex == that.groupIndex\n        && trackIndex == that.trackIndex;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = periodIndex;\n    result = 31 * result + groupIndex;\n    result = 31 * result + trackIndex;\n    return result;\n  }\n\n  // Comparable implementation.\n\n  @Override\n  public int compareTo(StreamKey o) {\n    int result = periodIndex - o.periodIndex;\n    if (result == 0) {\n      result = groupIndex - o.groupIndex;\n      if (result == 0) {\n        result = trackIndex - o.trackIndex;\n      }\n    }\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(periodIndex);\n    dest.writeInt(groupIndex);\n    dest.writeInt(trackIndex);\n  }\n\n  public static final Creator<StreamKey> CREATOR =\n      new Creator<StreamKey>() {\n\n        @Override\n        public StreamKey createFromParcel(Parcel in) {\n          return new StreamKey(in);\n        }\n\n        @Override\n        public StreamKey[] newArray(int size) {\n          return new StreamKey[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.offline;\n\nimport androidx.annotation.WorkerThread;\nimport java.io.IOException;\n\n/** A writable index of {@link Download Downloads}. */\n@WorkerThread\npublic interface WritableDownloadIndex extends DownloadIndex {\n\n  /**\n   * Adds or replaces a {@link Download}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param download The {@link Download} to be added.\n   * @throws IOException If an error occurs setting the state.\n   */\n  void putDownload(Download download) throws IOException;\n\n  /**\n   * Removes the download with the given ID. Does nothing if a download with the given ID does not\n   * exist.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param id The ID of the download to remove.\n   * @throws IOException If an error occurs removing the state.\n   */\n  void removeDownload(String id) throws IOException;\n\n  /**\n   * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @throws IOException If an error occurs updating the state.\n   */\n  void setDownloadingStatesToQueued() throws IOException;\n\n  /**\n   * Sets all states to {@link Download#STATE_REMOVING}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @throws IOException If an error occurs updating the state.\n   */\n  void setStatesToRemoving() throws IOException;\n\n  /**\n   * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED},\n   * {@link Download#STATE_FAILED}).\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param stopReason The stop reason.\n   * @throws IOException If an error occurs updating the state.\n   */\n  void setStopReason(int stopReason) throws IOException;\n\n  /**\n   * Sets the stop reason of the download with the given ID in a terminal state ({@link\n   * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the\n   * given ID does not exist, or if it's not in a terminal state.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param id The ID of the download to update.\n   * @param stopReason The stop reason.\n   * @throws IOException If an error occurs updating the state.\n   */\n  void setStopReason(String id, int stopReason) throws IOException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/offline/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.offline;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.scheduler;\n\nimport android.annotation.TargetApi;\nimport android.app.job.JobInfo;\nimport android.app.job.JobParameters;\nimport android.app.job.JobScheduler;\nimport android.app.job.JobService;\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.PersistableBundle;\nimport androidx.annotation.RequiresPermission;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link\n * PlatformSchedulerService} to your manifest:\n *\n * <pre>{@literal\n * <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>\n * <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n *\n * <service android:name=\"com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService\"\n *     android:permission=\"android.permission.BIND_JOB_SERVICE\"\n *     android:exported=\"true\"/>\n * }</pre>\n */\n@TargetApi(21)\npublic final class PlatformScheduler implements Scheduler {\n\n  private static final boolean DEBUG = false;\n  private static final String TAG = \"PlatformScheduler\";\n  private static final String KEY_SERVICE_ACTION = \"service_action\";\n  private static final String KEY_SERVICE_PACKAGE = \"service_package\";\n  private static final String KEY_REQUIREMENTS = \"requirements\";\n\n  private final int jobId;\n  private final ComponentName jobServiceComponentName;\n  private final JobScheduler jobScheduler;\n\n  /**\n   * @param context Any context.\n   * @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was\n   *     used by a previous instance, anything scheduled by the previous instance will be canceled\n   *     by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()}\n   *     are called.\n   */\n  @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)\n  public PlatformScheduler(Context context, int jobId) {\n    this.jobId = jobId;\n    jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class);\n    jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);\n  }\n\n  @Override\n  public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {\n    JobInfo jobInfo =\n        buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage);\n    int result = jobScheduler.schedule(jobInfo);\n    logd(\"Scheduling job: \" + jobId + \" result: \" + result);\n    return result == JobScheduler.RESULT_SUCCESS;\n  }\n\n  @Override\n  public boolean cancel() {\n    logd(\"Canceling job: \" + jobId);\n    jobScheduler.cancel(jobId);\n    return true;\n  }\n\n  // @RequiresPermission constructor annotation should ensure the permission is present.\n  @SuppressWarnings(\"MissingPermission\")\n  private static JobInfo buildJobInfo(\n      int jobId,\n      ComponentName jobServiceComponentName,\n      Requirements requirements,\n      String serviceAction,\n      String servicePackage) {\n    JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName);\n\n    if (requirements.isUnmeteredNetworkRequired()) {\n      builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);\n    } else if (requirements.isNetworkRequired()) {\n      builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);\n    }\n    builder.setRequiresDeviceIdle(requirements.isIdleRequired());\n    builder.setRequiresCharging(requirements.isChargingRequired());\n    builder.setPersisted(true);\n\n    PersistableBundle extras = new PersistableBundle();\n    extras.putString(KEY_SERVICE_ACTION, serviceAction);\n    extras.putString(KEY_SERVICE_PACKAGE, servicePackage);\n    extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());\n    builder.setExtras(extras);\n\n    return builder.build();\n  }\n\n  private static void logd(String message) {\n    if (DEBUG) {\n      Log.d(TAG, message);\n    }\n  }\n\n  /** A {@link JobService} that starts the target service if the requirements are met. */\n  public static final class PlatformSchedulerService extends JobService {\n    @Override\n    public boolean onStartJob(JobParameters params) {\n      logd(\"PlatformSchedulerService started\");\n      PersistableBundle extras = params.getExtras();\n      Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));\n      if (requirements.checkRequirements(this)) {\n        logd(\"Requirements are met\");\n        String serviceAction = extras.getString(KEY_SERVICE_ACTION);\n        String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);\n        Intent intent =\n            new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage);\n        logd(\"Starting service action: \" + serviceAction + \" package: \" + servicePackage);\n        Util.startForegroundService(this, intent);\n      } else {\n        logd(\"Requirements are not met\");\n        jobFinished(params, /* needsReschedule */ true);\n      }\n      return false;\n    }\n\n    @Override\n    public boolean onStopJob(JobParameters params) {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.scheduler;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.net.ConnectivityManager;\nimport android.net.Network;\nimport android.net.NetworkCapabilities;\nimport android.net.NetworkInfo;\nimport android.os.BatteryManager;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.os.PowerManager;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Defines a set of device state requirements. */\npublic final class Requirements implements Parcelable {\n\n  /**\n   * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED},\n   * {@link #DEVICE_IDLE} and {@link #DEVICE_CHARGING}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING})\n  public @interface RequirementFlags {}\n\n  /** Requirement that the device has network connectivity. */\n  public static final int NETWORK = 1;\n  /** Requirement that the device has a network connection that is unmetered. */\n  public static final int NETWORK_UNMETERED = 1 << 1;\n  /** Requirement that the device is idle. */\n  public static final int DEVICE_IDLE = 1 << 2;\n  /** Requirement that the device is charging. */\n  public static final int DEVICE_CHARGING = 1 << 3;\n\n  @RequirementFlags private final int requirements;\n\n  /** @param requirements A combination of requirement flags. */\n  public Requirements(@RequirementFlags int requirements) {\n    if ((requirements & NETWORK_UNMETERED) != 0) {\n      // Make sure network requirement flags are consistent.\n      requirements |= NETWORK;\n    }\n    this.requirements = requirements;\n  }\n\n  /** Returns the requirements. */\n  @RequirementFlags\n  public int getRequirements() {\n    return requirements;\n  }\n\n  /** Returns whether network connectivity is required. */\n  public boolean isNetworkRequired() {\n    return (requirements & NETWORK) != 0;\n  }\n\n  /** Returns whether un-metered network connectivity is required. */\n  public boolean isUnmeteredNetworkRequired() {\n    return (requirements & NETWORK_UNMETERED) != 0;\n  }\n\n  /** Returns whether the device is required to be charging. */\n  public boolean isChargingRequired() {\n    return (requirements & DEVICE_CHARGING) != 0;\n  }\n\n  /** Returns whether the device is required to be idle. */\n  public boolean isIdleRequired() {\n    return (requirements & DEVICE_IDLE) != 0;\n  }\n\n  /**\n   * Returns whether the requirements are met.\n   *\n   * @param context Any context.\n   * @return Whether the requirements are met.\n   */\n  public boolean checkRequirements(Context context) {\n    return getNotMetRequirements(context) == 0;\n  }\n\n  /**\n   * Returns requirements that are not met, or 0.\n   *\n   * @param context Any context.\n   * @return The requirements that are not met, or 0.\n   */\n  @RequirementFlags\n  public int getNotMetRequirements(Context context) {\n    @RequirementFlags int notMetRequirements = getNotMetNetworkRequirements(context);\n    if (isChargingRequired() && !isDeviceCharging(context)) {\n      notMetRequirements |= DEVICE_CHARGING;\n    }\n    if (isIdleRequired() && !isDeviceIdle(context)) {\n      notMetRequirements |= DEVICE_IDLE;\n    }\n    return notMetRequirements;\n  }\n\n  @RequirementFlags\n  private int getNotMetNetworkRequirements(Context context) {\n    if (!isNetworkRequired()) {\n      return 0;\n    }\n\n    ConnectivityManager connectivityManager =\n        (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n    NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo();\n    if (networkInfo == null\n        || !networkInfo.isConnected()\n        || !isInternetConnectivityValidated(connectivityManager)) {\n      return requirements & (NETWORK | NETWORK_UNMETERED);\n    }\n\n    if (isUnmeteredNetworkRequired() && connectivityManager.isActiveNetworkMetered()) {\n      return NETWORK_UNMETERED;\n    }\n\n    return 0;\n  }\n\n  private boolean isDeviceCharging(Context context) {\n    Intent batteryStatus =\n        context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));\n    if (batteryStatus == null) {\n      return false;\n    }\n    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);\n    return status == BatteryManager.BATTERY_STATUS_CHARGING\n        || status == BatteryManager.BATTERY_STATUS_FULL;\n  }\n\n  private boolean isDeviceIdle(Context context) {\n    PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);\n    return Util.SDK_INT >= 23\n        ? powerManager.isDeviceIdleMode()\n        : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();\n  }\n\n  private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {\n    if (Util.SDK_INT < 23) {\n      // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API\n      // levels prior to 23.\n      return true;\n    }\n    Network activeNetwork = connectivityManager.getActiveNetwork();\n    if (activeNetwork == null) {\n      return false;\n    }\n    NetworkCapabilities networkCapabilities =\n        connectivityManager.getNetworkCapabilities(activeNetwork);\n    boolean validated =\n        networkCapabilities == null\n            || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);\n    return !validated;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    return requirements == ((Requirements) o).requirements;\n  }\n\n  @Override\n  public int hashCode() {\n    return requirements;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(requirements);\n  }\n\n  public static final Creator<Requirements> CREATOR =\n      new Creator<Requirements>() {\n\n        @Override\n        public Requirements createFromParcel(Parcel in) {\n          return new Requirements(in.readInt());\n        }\n\n        @Override\n        public Requirements[] newArray(int size) {\n          return new Requirements[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.scheduler;\n\nimport android.annotation.TargetApi;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.net.ConnectivityManager;\nimport android.net.Network;\nimport android.net.NetworkCapabilities;\nimport android.net.NetworkRequest;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.PowerManager;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes.\n */\npublic final class RequirementsWatcher {\n\n  /**\n   * Notified when RequirementsWatcher instance first created and on changes whether the {@link\n   * Requirements} are met.\n   */\n  public interface Listener {\n    /**\n     * Called when there is a change on the met requirements.\n     *\n     * @param requirementsWatcher Calling instance.\n     * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not\n     *     met, or 0.\n     */\n    void onRequirementsStateChanged(\n            RequirementsWatcher requirementsWatcher,\n            @Requirements.RequirementFlags int notMetRequirements);\n  }\n\n  private final Context context;\n  private final Listener listener;\n  private final Requirements requirements;\n  private final Handler handler;\n\n  @Nullable private DeviceStatusChangeReceiver receiver;\n\n  @Requirements.RequirementFlags private int notMetRequirements;\n  @Nullable private CapabilityValidatedCallback networkCallback;\n\n  /**\n   * @param context Any context.\n   * @param listener Notified whether the {@link Requirements} are met.\n   * @param requirements The requirements to watch.\n   */\n  public RequirementsWatcher(Context context, Listener listener, Requirements requirements) {\n    this.context = context.getApplicationContext();\n    this.listener = listener;\n    this.requirements = requirements;\n    handler = new Handler(Util.getLooper());\n  }\n\n  /**\n   * Starts watching for changes. Must be called from a thread that has an associated {@link\n   * Looper}. Listener methods are called on the caller thread.\n   *\n   * @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0.\n   */\n  @Requirements.RequirementFlags\n  public int start() {\n    notMetRequirements = requirements.getNotMetRequirements(context);\n\n    IntentFilter filter = new IntentFilter();\n    if (requirements.isNetworkRequired()) {\n      if (Util.SDK_INT >= 23) {\n        registerNetworkCallbackV23();\n      } else {\n        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);\n      }\n    }\n    if (requirements.isChargingRequired()) {\n      filter.addAction(Intent.ACTION_POWER_CONNECTED);\n      filter.addAction(Intent.ACTION_POWER_DISCONNECTED);\n    }\n    if (requirements.isIdleRequired()) {\n      if (Util.SDK_INT >= 23) {\n        filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);\n      } else {\n        filter.addAction(Intent.ACTION_SCREEN_ON);\n        filter.addAction(Intent.ACTION_SCREEN_OFF);\n      }\n    }\n    receiver = new DeviceStatusChangeReceiver();\n    context.registerReceiver(receiver, filter, null, handler);\n    return notMetRequirements;\n  }\n\n  /** Stops watching for changes. */\n  public void stop() {\n    context.unregisterReceiver(Assertions.checkNotNull(receiver));\n    receiver = null;\n    if (networkCallback != null) {\n      unregisterNetworkCallback();\n    }\n  }\n\n  /** Returns watched {@link Requirements}. */\n  public Requirements getRequirements() {\n    return requirements;\n  }\n\n  @TargetApi(23)\n  private void registerNetworkCallbackV23() {\n    ConnectivityManager connectivityManager =\n        Assertions.checkNotNull(\n            (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));\n    NetworkRequest request =\n        new NetworkRequest.Builder()\n            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)\n            .build();\n    networkCallback = new CapabilityValidatedCallback();\n    connectivityManager.registerNetworkCallback(request, networkCallback);\n  }\n\n  private void unregisterNetworkCallback() {\n    if (Util.SDK_INT >= 21) {\n      ConnectivityManager connectivityManager =\n          (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n      connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback));\n      networkCallback = null;\n    }\n  }\n\n  private void checkRequirements() {\n    @Requirements.RequirementFlags\n    int notMetRequirements = requirements.getNotMetRequirements(context);\n    if (this.notMetRequirements != notMetRequirements) {\n      this.notMetRequirements = notMetRequirements;\n      listener.onRequirementsStateChanged(this, notMetRequirements);\n    }\n  }\n\n  private class DeviceStatusChangeReceiver extends BroadcastReceiver {\n    @Override\n    public void onReceive(Context context, Intent intent) {\n      if (!isInitialStickyBroadcast()) {\n        checkRequirements();\n      }\n    }\n  }\n\n  @RequiresApi(api = 21)\n  private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {\n    @Override\n    public void onAvailable(Network network) {\n      onNetworkCallback();\n    }\n\n    @Override\n    public void onLost(Network network) {\n      onNetworkCallback();\n    }\n\n    private void onNetworkCallback() {\n      handler.post(\n          () -> {\n            if (networkCallback != null) {\n              checkRequirements();\n            }\n          });\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.scheduler;\n\nimport android.app.Notification;\nimport android.app.Service;\nimport android.content.Intent;\n\n/** Schedules a service to be started in the foreground when some {@link Requirements} are met. */\npublic interface Scheduler {\n\n  /**\n   * Schedules a service to be started in the foreground when some {@link Requirements} are met.\n   * Anything that was previously scheduled will be canceled.\n   *\n   * <p>The service to be started must be declared in the manifest of {@code servicePackage} with an\n   * intent filter containing {@code serviceAction}. Note that when started with {@code\n   * serviceAction}, the service must call {@link Service#startForeground(int, Notification)} to\n   * make itself a foreground service, as documented by {@link\n   * Service#startForegroundService(Intent)}.\n   *\n   * @param requirements The requirements.\n   * @param servicePackage The package name.\n   * @param serviceAction The action with which the service will be started.\n   * @return Whether scheduling was successful.\n   */\n  boolean schedule(Requirements requirements, String servicePackage, String serviceAction);\n\n  /**\n   * Cancels anything that was previously scheduled, or else does nothing.\n   *\n   * @return Whether cancellation was successful.\n   */\n  boolean cancel();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.scheduler;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/** Abstract base class for the concatenation of one or more {@link Timeline}s. */\n/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {\n\n  private final int childCount;\n  private final ShuffleOrder shuffleOrder;\n  private final boolean isAtomic;\n\n  /**\n   * Returns UID of child timeline from a concatenated period UID.\n   *\n   * @param concatenatedUid UID of a period in a concatenated timeline.\n   * @return UID of the child timeline this period belongs to.\n   */\n  @SuppressWarnings(\"nullness:return.type.incompatible\")\n  public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) {\n    return ((Pair<?, ?>) concatenatedUid).first;\n  }\n\n  /**\n   * Returns UID of the period in the child timeline from a concatenated period UID.\n   *\n   * @param concatenatedUid UID of a period in a concatenated timeline.\n   * @return UID of the period in the child timeline.\n   */\n  @SuppressWarnings(\"nullness:return.type.incompatible\")\n  public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) {\n    return ((Pair<?, ?>) concatenatedUid).second;\n  }\n\n  /**\n   * Returns a concatenated UID for a period or window in a child timeline.\n   *\n   * @param childTimelineUid UID of the child timeline this period or window belongs to.\n   * @param childPeriodOrWindowUid UID of the period or window in the child timeline.\n   * @return UID of the period or window in the concatenated timeline.\n   */\n  public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodOrWindowUid) {\n    return Pair.create(childTimelineUid, childPeriodOrWindowUid);\n  }\n\n  /**\n   * Sets up a concatenated timeline with a shuffle order of child timelines.\n   *\n   * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a\n   *     single item for repeating and shuffling.\n   * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must\n   *     match the number of elements in the shuffle order.\n   */\n  public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) {\n    this.isAtomic = isAtomic;\n    this.shuffleOrder = shuffleOrder;\n    this.childCount = shuffleOrder.getLength();\n  }\n\n  @Override\n  public int getNextWindowIndex(\n      int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {\n    if (isAtomic) {\n      // Adapt repeat and shuffle mode to atomic concatenation.\n      repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;\n      shuffleModeEnabled = false;\n    }\n    // Find next window within current child.\n    int childIndex = getChildIndexByWindowIndex(windowIndex);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    int nextWindowIndexInChild =\n        getTimelineByChildIndex(childIndex)\n            .getNextWindowIndex(\n                windowIndex - firstWindowIndexInChild,\n                repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,\n                shuffleModeEnabled);\n    if (nextWindowIndexInChild != C.INDEX_UNSET) {\n      return firstWindowIndexInChild + nextWindowIndexInChild;\n    }\n    // If not found, find first window of next non-empty child.\n    int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled);\n    while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) {\n      nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled);\n    }\n    if (nextChildIndex != C.INDEX_UNSET) {\n      return getFirstWindowIndexByChildIndex(nextChildIndex)\n          + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled);\n    }\n    // If not found, this is the last window.\n    if (repeatMode == Player.REPEAT_MODE_ALL) {\n      return getFirstWindowIndex(shuffleModeEnabled);\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getPreviousWindowIndex(\n      int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {\n    if (isAtomic) {\n      // Adapt repeat and shuffle mode to atomic concatenation.\n      repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;\n      shuffleModeEnabled = false;\n    }\n    // Find previous window within current child.\n    int childIndex = getChildIndexByWindowIndex(windowIndex);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    int previousWindowIndexInChild =\n        getTimelineByChildIndex(childIndex)\n            .getPreviousWindowIndex(\n                windowIndex - firstWindowIndexInChild,\n                repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,\n                shuffleModeEnabled);\n    if (previousWindowIndexInChild != C.INDEX_UNSET) {\n      return firstWindowIndexInChild + previousWindowIndexInChild;\n    }\n    // If not found, find last window of previous non-empty child.\n    int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled);\n    while (previousChildIndex != C.INDEX_UNSET\n        && getTimelineByChildIndex(previousChildIndex).isEmpty()) {\n      previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled);\n    }\n    if (previousChildIndex != C.INDEX_UNSET) {\n      return getFirstWindowIndexByChildIndex(previousChildIndex)\n          + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled);\n    }\n    // If not found, this is the first window.\n    if (repeatMode == Player.REPEAT_MODE_ALL) {\n      return getLastWindowIndex(shuffleModeEnabled);\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getLastWindowIndex(boolean shuffleModeEnabled) {\n    if (childCount == 0) {\n      return C.INDEX_UNSET;\n    }\n    if (isAtomic) {\n      shuffleModeEnabled = false;\n    }\n    // Find last non-empty child.\n    int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1;\n    while (getTimelineByChildIndex(lastChildIndex).isEmpty()) {\n      lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled);\n      if (lastChildIndex == C.INDEX_UNSET) {\n        // All children are empty.\n        return C.INDEX_UNSET;\n      }\n    }\n    return getFirstWindowIndexByChildIndex(lastChildIndex)\n        + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled);\n  }\n\n  @Override\n  public int getFirstWindowIndex(boolean shuffleModeEnabled) {\n    if (childCount == 0) {\n      return C.INDEX_UNSET;\n    }\n    if (isAtomic) {\n      shuffleModeEnabled = false;\n    }\n    // Find first non-empty child.\n    int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0;\n    while (getTimelineByChildIndex(firstChildIndex).isEmpty()) {\n      firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled);\n      if (firstChildIndex == C.INDEX_UNSET) {\n        // All children are empty.\n        return C.INDEX_UNSET;\n      }\n    }\n    return getFirstWindowIndexByChildIndex(firstChildIndex)\n        + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled);\n  }\n\n  @Override\n  public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n    int childIndex = getChildIndexByWindowIndex(windowIndex);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);\n    getTimelineByChildIndex(childIndex)\n        .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs);\n    Object childUid = getChildUidByChildIndex(childIndex);\n    // Don't create new objects if the child is using SINGLE_WINDOW_UID.\n    window.uid =\n        Window.SINGLE_WINDOW_UID.equals(window.uid)\n            ? childUid\n            : getConcatenatedUid(childUid, window.uid);\n    window.firstPeriodIndex += firstPeriodIndexInChild;\n    window.lastPeriodIndex += firstPeriodIndexInChild;\n    return window;\n  }\n\n  @Override\n  public final Period getPeriodByUid(Object uid, Period period) {\n    Object childUid = getChildTimelineUidFromConcatenatedUid(uid);\n    Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);\n    int childIndex = getChildIndexByChildUid(childUid);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    getTimelineByChildIndex(childIndex).getPeriodByUid(periodUid, period);\n    period.windowIndex += firstWindowIndexInChild;\n    period.uid = uid;\n    return period;\n  }\n\n  @Override\n  public final Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    int childIndex = getChildIndexByPeriodIndex(periodIndex);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);\n    getTimelineByChildIndex(childIndex)\n        .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);\n    period.windowIndex += firstWindowIndexInChild;\n    if (setIds) {\n      period.uid =\n          getConcatenatedUid(\n              getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid));\n    }\n    return period;\n  }\n\n  @Override\n  public final int getIndexOfPeriod(Object uid) {\n    if (!(uid instanceof Pair)) {\n      return C.INDEX_UNSET;\n    }\n    Object childUid = getChildTimelineUidFromConcatenatedUid(uid);\n    Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);\n    int childIndex = getChildIndexByChildUid(childUid);\n    if (childIndex == C.INDEX_UNSET) {\n      return C.INDEX_UNSET;\n    }\n    int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid);\n    return periodIndexInChild == C.INDEX_UNSET\n        ? C.INDEX_UNSET\n        : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild;\n  }\n\n  @Override\n  public final Object getUidOfPeriod(int periodIndex) {\n    int childIndex = getChildIndexByPeriodIndex(periodIndex);\n    int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);\n    Object periodUidInChild =\n        getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild);\n    return getConcatenatedUid(getChildUidByChildIndex(childIndex), periodUidInChild);\n  }\n\n  /**\n   * Returns the index of the child timeline containing the given period index.\n   *\n   * @param periodIndex A valid period index within the bounds of the timeline.\n   */\n  protected abstract int getChildIndexByPeriodIndex(int periodIndex);\n\n  /**\n   * Returns the index of the child timeline containing the given window index.\n   *\n   * @param windowIndex A valid window index within the bounds of the timeline.\n   */\n  protected abstract int getChildIndexByWindowIndex(int windowIndex);\n\n  /**\n   * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not\n   * found.\n   *\n   * @param childUid A child UID.\n   * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found.\n   */\n  protected abstract int getChildIndexByChildUid(Object childUid);\n\n  /**\n   * Returns the child timeline for the child with the given index.\n   *\n   * @param childIndex A valid child index within the bounds of the timeline.\n   */\n  protected abstract Timeline getTimelineByChildIndex(int childIndex);\n\n  /**\n   * Returns the first period index belonging to the child timeline with the given index.\n   *\n   * @param childIndex A valid child index within the bounds of the timeline.\n   */\n  protected abstract int getFirstPeriodIndexByChildIndex(int childIndex);\n\n  /**\n   * Returns the first window index belonging to the child timeline with the given index.\n   *\n   * @param childIndex A valid child index within the bounds of the timeline.\n   */\n  protected abstract int getFirstWindowIndexByChildIndex(int childIndex);\n\n  /**\n   * Returns the UID of the child timeline with the given index.\n   *\n   * @param childIndex A valid child index within the bounds of the timeline.\n   */\n  protected abstract Object getChildUidByChildIndex(int childIndex);\n\n  private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) {\n    return shuffleModeEnabled\n        ? shuffleOrder.getNextIndex(childIndex)\n        : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;\n  }\n\n  private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) {\n    return shuffleModeEnabled\n        ? shuffleOrder.getPreviousIndex(childIndex)\n        : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\n/**\n * Interface for callbacks to be notified of {@link MediaSource} events.\n *\n * @deprecated Use {@link MediaSourceEventListener}.\n */\n@Deprecated\npublic interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\nimport java.util.HashSet;\n\n/**\n * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link\n * MediaSourceEventListener}s.\n *\n * <p>Whenever an implementing subclass needs to provide a new timeline, it must call {@link\n * #refreshSourceInfo(Timeline)} to notify all listeners.\n */\npublic abstract class BaseMediaSource implements MediaSource {\n\n  private final ArrayList<MediaSourceCaller> mediaSourceCallers;\n  private final HashSet<MediaSourceCaller> enabledMediaSourceCallers;\n  private final MediaSourceEventListener.EventDispatcher eventDispatcher;\n\n  @Nullable private Looper looper;\n  @Nullable private Timeline timeline;\n\n  public BaseMediaSource() {\n    mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1);\n    enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1);\n    eventDispatcher = new MediaSourceEventListener.EventDispatcher();\n  }\n\n  /**\n   * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller,\n   * TransferListener)}. This method is called at most once until the next call to {@link\n   * #releaseSourceInternal()}.\n   *\n   * @param mediaTransferListener The transfer listener which should be informed of any media data\n   *     transfers. May be null if no listener is available. Note that this listener should usually\n   *     be only informed of transfers related to the media loads and not of auxiliary loads for\n   *     manifests and other data.\n   */\n  protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener);\n\n  /** Enables the source, see {@link #enable(MediaSourceCaller)}. */\n  protected void enableInternal() {}\n\n  /** Disables the source, see {@link #disable(MediaSourceCaller)}. */\n  protected void disableInternal() {}\n\n  /**\n   * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called\n   * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}.\n   */\n  protected abstract void releaseSourceInternal();\n\n  /**\n   * Updates timeline and manifest and notifies all listeners of the update.\n   *\n   * @param timeline The new {@link Timeline}.\n   */\n  protected final void refreshSourceInfo(Timeline timeline) {\n    this.timeline = timeline;\n    for (MediaSourceCaller caller : mediaSourceCallers) {\n      caller.onSourceInfoRefreshed(/* source= */ this, timeline);\n    }\n  }\n\n  /**\n   * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the\n   * registered listeners with the specified media period id.\n   *\n   * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if\n   *     the events do not belong to a specific media period.\n   * @return An event dispatcher with pre-configured media period id.\n   */\n  protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(\n      @Nullable MediaPeriodId mediaPeriodId) {\n    return eventDispatcher.withParameters(\n        /* windowIndex= */ 0, mediaPeriodId, /* mediaTimeOffsetMs= */ 0);\n  }\n\n  /**\n   * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the\n   * registered listeners with the specified media period id and time offset.\n   *\n   * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.\n   * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.\n   * @return An event dispatcher with pre-configured media period id and time offset.\n   */\n  protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(\n      MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {\n    Assertions.checkArgument(mediaPeriodId != null);\n    return eventDispatcher.withParameters(/* windowIndex= */ 0, mediaPeriodId, mediaTimeOffsetMs);\n  }\n\n  /**\n   * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the\n   * registered listeners with the specified window index, media period id and time offset.\n   *\n   * @param windowIndex The timeline window index to be reported with the events.\n   * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if\n   *     the events do not belong to a specific media period.\n   * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.\n   * @return An event dispatcher with pre-configured media period id and time offset.\n   */\n  protected final MediaSourceEventListener.EventDispatcher createEventDispatcher(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {\n    return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs);\n  }\n\n  /** Returns whether the source is enabled. */\n  protected final boolean isEnabled() {\n    return !enabledMediaSourceCallers.isEmpty();\n  }\n\n  @Override\n  public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) {\n    eventDispatcher.addEventListener(handler, eventListener);\n  }\n\n  @Override\n  public final void removeEventListener(MediaSourceEventListener eventListener) {\n    eventDispatcher.removeEventListener(eventListener);\n  }\n\n  @Override\n  public final void prepareSource(\n      MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) {\n    Looper looper = Looper.myLooper();\n    Assertions.checkArgument(this.looper == null || this.looper == looper);\n    Timeline timeline = this.timeline;\n    mediaSourceCallers.add(caller);\n    if (this.looper == null) {\n      this.looper = looper;\n      enabledMediaSourceCallers.add(caller);\n      prepareSourceInternal(mediaTransferListener);\n    } else if (timeline != null) {\n      enable(caller);\n      caller.onSourceInfoRefreshed(/* source= */ this, timeline);\n    }\n  }\n\n  @Override\n  public final void enable(MediaSourceCaller caller) {\n    Assertions.checkNotNull(looper);\n    boolean wasDisabled = enabledMediaSourceCallers.isEmpty();\n    enabledMediaSourceCallers.add(caller);\n    if (wasDisabled) {\n      enableInternal();\n    }\n  }\n\n  @Override\n  public final void disable(MediaSourceCaller caller) {\n    boolean wasEnabled = !enabledMediaSourceCallers.isEmpty();\n    enabledMediaSourceCallers.remove(caller);\n    if (wasEnabled && enabledMediaSourceCallers.isEmpty()) {\n      disableInternal();\n    }\n  }\n\n  @Override\n  public final void releaseSource(MediaSourceCaller caller) {\n    mediaSourceCallers.remove(caller);\n    if (mediaSourceCallers.isEmpty()) {\n      looper = null;\n      timeline = null;\n      enabledMediaSourceCallers.clear();\n      releaseSourceInternal();\n    } else {\n      disable(caller);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport java.io.IOException;\n\n/**\n * Thrown when a live playback falls behind the available media window.\n */\npublic final class BehindLiveWindowException extends IOException {\n\n  public BehindLiveWindowException() {\n    super();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their\n * samples.\n */\npublic final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {\n\n  /**\n   * The {@link MediaPeriod} wrapped by this clipping media period.\n   */\n  public final MediaPeriod mediaPeriod;\n\n  @Nullable private Callback callback;\n  private @NullableType ClippingSampleStream[] sampleStreams;\n  private long pendingInitialDiscontinuityPositionUs;\n  /* package */ long startUs;\n  /* package */ long endUs;\n\n  /**\n   * Creates a new clipping media period that provides a clipped view of the specified {@link\n   * MediaPeriod}'s sample streams.\n   *\n   * <p>If the start point is guaranteed to be a key frame, pass {@code false} to {@code\n   * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is\n   * first read from.\n   *\n   * @param mediaPeriod The media period to clip.\n   * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.\n   * @param startUs The clipping start time, in microseconds.\n   * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to\n   *     indicate the end of the period.\n   */\n  public ClippingMediaPeriod(\n      MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity, long startUs, long endUs) {\n    this.mediaPeriod = mediaPeriod;\n    sampleStreams = new ClippingSampleStream[0];\n    pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? startUs : C.TIME_UNSET;\n    this.startUs = startUs;\n    this.endUs = endUs;\n  }\n\n  /**\n   * Updates the clipping start/end times for this period, in microseconds.\n   *\n   * @param startUs The clipping start time, in microseconds.\n   * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to\n   *     indicate the end of the period.\n   */\n  public void updateClipping(long startUs, long endUs) {\n    this.startUs = startUs;\n    this.endUs = endUs;\n  }\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    mediaPeriod.prepare(this, positionUs);\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    mediaPeriod.maybeThrowPrepareError();\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return mediaPeriod.getTrackGroups();\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    sampleStreams = new ClippingSampleStream[streams.length];\n    @NullableType SampleStream[] childStreams = new SampleStream[streams.length];\n    for (int i = 0; i < streams.length; i++) {\n      sampleStreams[i] = (ClippingSampleStream) streams[i];\n      childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null;\n    }\n    long enablePositionUs =\n        mediaPeriod.selectTracks(\n            selections, mayRetainStreamFlags, childStreams, streamResetFlags, positionUs);\n    pendingInitialDiscontinuityPositionUs =\n        isPendingInitialDiscontinuity()\n                && positionUs == startUs\n                && shouldKeepInitialDiscontinuity(startUs, selections)\n            ? enablePositionUs\n            : C.TIME_UNSET;\n    Assertions.checkState(\n        enablePositionUs == positionUs\n            || (enablePositionUs >= startUs\n                && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));\n    for (int i = 0; i < streams.length; i++) {\n      if (childStreams[i] == null) {\n        sampleStreams[i] = null;\n      } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) {\n        sampleStreams[i] = new ClippingSampleStream(childStreams[i]);\n      }\n      streams[i] = sampleStreams[i];\n    }\n    return enablePositionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    mediaPeriod.discardBuffer(positionUs, toKeyframe);\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    mediaPeriod.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (isPendingInitialDiscontinuity()) {\n      long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs;\n      pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;\n      // Always read an initial discontinuity from the child, and use it if set.\n      long childDiscontinuityUs = readDiscontinuity();\n      return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs;\n    }\n    long discontinuityUs = mediaPeriod.readDiscontinuity();\n    if (discontinuityUs == C.TIME_UNSET) {\n      return C.TIME_UNSET;\n    }\n    Assertions.checkState(discontinuityUs >= startUs);\n    Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);\n    return discontinuityUs;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();\n    if (bufferedPositionUs == C.TIME_END_OF_SOURCE\n        || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {\n      return C.TIME_END_OF_SOURCE;\n    }\n    return bufferedPositionUs;\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;\n    for (ClippingSampleStream sampleStream : sampleStreams) {\n      if (sampleStream != null) {\n        sampleStream.clearSentEos();\n      }\n    }\n    long seekUs = mediaPeriod.seekToUs(positionUs);\n    Assertions.checkState(\n        seekUs == positionUs\n            || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));\n    return seekUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    if (positionUs == startUs) {\n      // Never adjust seeks to the start of the clipped view.\n      return startUs;\n    }\n    SeekParameters clippedSeekParameters = clipSeekParameters(positionUs, seekParameters);\n    return mediaPeriod.getAdjustedSeekPositionUs(positionUs, clippedSeekParameters);\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();\n    if (nextLoadPositionUs == C.TIME_END_OF_SOURCE\n        || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {\n      return C.TIME_END_OF_SOURCE;\n    }\n    return nextLoadPositionUs;\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    return mediaPeriod.continueLoading(positionUs);\n  }\n\n  @Override\n  public boolean isLoading() {\n    return mediaPeriod.isLoading();\n  }\n\n  // MediaPeriod.Callback implementation.\n\n  @Override\n  public void onPrepared(MediaPeriod mediaPeriod) {\n    Assertions.checkNotNull(callback).onPrepared(this);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod source) {\n    Assertions.checkNotNull(callback).onContinueLoadingRequested(this);\n  }\n\n  /* package */ boolean isPendingInitialDiscontinuity() {\n    return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET;\n  }\n\n  private SeekParameters clipSeekParameters(long positionUs, SeekParameters seekParameters) {\n    long toleranceBeforeUs =\n        Util.constrainValue(\n            seekParameters.toleranceBeforeUs, /* min= */ 0, /* max= */ positionUs - startUs);\n    long toleranceAfterUs =\n        Util.constrainValue(\n            seekParameters.toleranceAfterUs,\n            /* min= */ 0,\n            /* max= */ endUs == C.TIME_END_OF_SOURCE ? Long.MAX_VALUE : endUs - positionUs);\n    if (toleranceBeforeUs == seekParameters.toleranceBeforeUs\n        && toleranceAfterUs == seekParameters.toleranceAfterUs) {\n      return seekParameters;\n    } else {\n      return new SeekParameters(toleranceBeforeUs, toleranceAfterUs);\n    }\n  }\n\n  private static boolean shouldKeepInitialDiscontinuity(\n      long startUs, @NullableType TrackSelection[] selections) {\n    // If the clipping start position is non-zero, the clipping sample streams will adjust\n    // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer\n    // timestamps can be negative, because sample streams provide buffers starting at a key-frame,\n    // which may be before the clipping start point. When the renderer reads a buffer with a\n    // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp\n    // read in the previous period. Renderer implementations may not allow this, so we signal a\n    // discontinuity which resets the renderers before they read the clipping sample stream.\n    // However, for audio-only track selections we assume to have random access seek behaviour and\n    // do not need an initial discontinuity to reset the renderer.\n    if (startUs != 0) {\n      for (TrackSelection trackSelection : selections) {\n        if (trackSelection != null) {\n          Format selectedFormat = trackSelection.getSelectedFormat();\n          if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {\n            return true;\n          }\n        }\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Wraps a {@link SampleStream} and clips its samples.\n   */\n  private final class ClippingSampleStream implements SampleStream {\n\n    public final SampleStream childStream;\n\n    private boolean sentEos;\n\n    public ClippingSampleStream(SampleStream childStream) {\n      this.childStream = childStream;\n    }\n\n    public void clearSentEos() {\n      sentEos = false;\n    }\n\n    @Override\n    public boolean isReady() {\n      return !isPendingInitialDiscontinuity() && childStream.isReady();\n    }\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      childStream.maybeThrowError();\n    }\n\n    @Override\n    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n        boolean requireFormat) {\n      if (isPendingInitialDiscontinuity()) {\n        return C.RESULT_NOTHING_READ;\n      }\n      if (sentEos) {\n        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n        return C.RESULT_BUFFER_READ;\n      }\n      int result = childStream.readData(formatHolder, buffer, requireFormat);\n      if (result == C.RESULT_FORMAT_READ) {\n        Format format = Assertions.checkNotNull(formatHolder.format);\n        if (format.encoderDelay != 0 || format.encoderPadding != 0) {\n          // Clear gapless playback metadata if the start/end points don't match the media.\n          int encoderDelay = startUs != 0 ? 0 : format.encoderDelay;\n          int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding;\n          formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);\n        }\n        return C.RESULT_FORMAT_READ;\n      }\n      if (endUs != C.TIME_END_OF_SOURCE\n          && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)\n              || (result == C.RESULT_NOTHING_READ\n                  && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {\n        buffer.clear();\n        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n        sentEos = true;\n        return C.RESULT_BUFFER_READ;\n      }\n      return result;\n    }\n\n    @Override\n    public int skipData(long positionUs) {\n      if (isPendingInitialDiscontinuity()) {\n        return C.RESULT_NOTHING_READ;\n      }\n      return childStream.skipData(positionUs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\n\n/**\n * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end\n * positions. The wrapped source must consist of a single period.\n */\npublic final class ClippingMediaSource extends CompositeMediaSource<Void> {\n\n  /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */\n  public static final class IllegalClippingException extends IOException {\n\n    /**\n     * The reason clipping failed. One of {@link #REASON_INVALID_PERIOD_COUNT}, {@link\n     * #REASON_NOT_SEEKABLE_TO_START} or {@link #REASON_START_EXCEEDS_END}.\n     */\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END})\n    public @interface Reason {}\n    /** The wrapped source doesn't consist of a single period. */\n    public static final int REASON_INVALID_PERIOD_COUNT = 0;\n    /** The wrapped source is not seekable and a non-zero clipping start position was specified. */\n    public static final int REASON_NOT_SEEKABLE_TO_START = 1;\n    /** The wrapped source ends before the specified clipping start position. */\n    public static final int REASON_START_EXCEEDS_END = 2;\n\n    /** The reason clipping failed. */\n    public final @Reason int reason;\n\n    /**\n     * @param reason The reason clipping failed.\n     */\n    public IllegalClippingException(@Reason int reason) {\n      super(\"Illegal clipping: \" + getReasonDescription(reason));\n      this.reason = reason;\n    }\n\n    private static String getReasonDescription(@Reason int reason) {\n      switch (reason) {\n        case REASON_INVALID_PERIOD_COUNT:\n          return \"invalid period count\";\n        case REASON_NOT_SEEKABLE_TO_START:\n          return \"not seekable to start\";\n        case REASON_START_EXCEEDS_END:\n          return \"start exceeds end\";\n        default:\n          return \"unknown\";\n      }\n    }\n  }\n\n  private final MediaSource mediaSource;\n  private final long startUs;\n  private final long endUs;\n  private final boolean enableInitialDiscontinuity;\n  private final boolean allowDynamicClippingUpdates;\n  private final boolean relativeToDefaultPosition;\n  private final ArrayList<ClippingMediaPeriod> mediaPeriods;\n  private final Timeline.Window window;\n\n  @Nullable private ClippingTimeline clippingTimeline;\n  @Nullable private IllegalClippingException clippingError;\n  private long periodStartUs;\n  private long periodEndUs;\n\n  /**\n   * Creates a new clipping source that wraps the specified source and provides samples between the\n   * specified start and end position.\n   *\n   * @param mediaSource The single-period source to wrap.\n   * @param startPositionUs The start position within {@code mediaSource}'s window at which to start\n   *     providing samples, in microseconds.\n   * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop\n   *     providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples\n   *     from the specified start point up to the end of the source. Specifying a position that\n   *     exceeds the {@code mediaSource}'s duration will also result in the end of the source not\n   *     being clipped.\n   */\n  public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {\n    this(\n        mediaSource,\n        startPositionUs,\n        endPositionUs,\n        /* enableInitialDiscontinuity= */ true,\n        /* allowDynamicClippingUpdates= */ false,\n        /* relativeToDefaultPosition= */ false);\n  }\n\n  /**\n   * Creates a new clipping source that wraps the specified source and provides samples from the\n   * default position for the specified duration.\n   *\n   * @param mediaSource The single-period source to wrap.\n   * @param durationUs The duration from the default position in the window in {@code mediaSource}'s\n   *     timeline at which to stop providing samples. Specifying a duration that exceeds the {@code\n   *     mediaSource}'s duration will result in the end of the source not being clipped.\n   */\n  public ClippingMediaSource(MediaSource mediaSource, long durationUs) {\n    this(\n        mediaSource,\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ durationUs,\n        /* enableInitialDiscontinuity= */ true,\n        /* allowDynamicClippingUpdates= */ false,\n        /* relativeToDefaultPosition= */ true);\n  }\n\n  /**\n   * Creates a new clipping source that wraps the specified source.\n   *\n   * <p>If the start point is guaranteed to be a key frame, pass {@code false} to {@code\n   * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first\n   * read from.\n   *\n   * <p>For live streams, if the clipping positions should move with the live window, pass {@code\n   * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback\n   * reaches {@code endPositionUs} in the last reported live window at the time a media period was\n   * created.\n   *\n   * @param mediaSource The single-period source to wrap.\n   * @param startPositionUs The start position at which to start providing samples, in microseconds.\n   *     If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the\n   *     start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition}\n   *     is {@code true}, this position is relative to the default position in the window in {@code\n   *     mediaSource}'s timeline.\n   * @param endPositionUs The end position at which to stop providing samples, in microseconds.\n   *     Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up\n   *     to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s\n   *     duration will also result in the end of the source not being clipped. If {@code\n   *     relativeToDefaultPosition} is {@code false}, the specified position is relative to the\n   *     start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition}\n   *     is {@code true}, this position is relative to the default position in the window in {@code\n   *     mediaSource}'s timeline.\n   * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled.\n   * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a\n   *     live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the\n   *     last reported live window at the time a media period was created.\n   * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are\n   *     relative to the default position in the window in {@code mediaSource}'s timeline.\n   */\n  public ClippingMediaSource(\n      MediaSource mediaSource,\n      long startPositionUs,\n      long endPositionUs,\n      boolean enableInitialDiscontinuity,\n      boolean allowDynamicClippingUpdates,\n      boolean relativeToDefaultPosition) {\n    Assertions.checkArgument(startPositionUs >= 0);\n    this.mediaSource = Assertions.checkNotNull(mediaSource);\n    startUs = startPositionUs;\n    endUs = endPositionUs;\n    this.enableInitialDiscontinuity = enableInitialDiscontinuity;\n    this.allowDynamicClippingUpdates = allowDynamicClippingUpdates;\n    this.relativeToDefaultPosition = relativeToDefaultPosition;\n    mediaPeriods = new ArrayList<>();\n    window = new Timeline.Window();\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return mediaSource.getTag();\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    prepareChildSource(/* id= */ null, mediaSource);\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    if (clippingError != null) {\n      throw clippingError;\n    }\n    super.maybeThrowSourceInfoRefreshError();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    ClippingMediaPeriod mediaPeriod =\n        new ClippingMediaPeriod(\n            mediaSource.createPeriod(id, allocator, startPositionUs),\n            enableInitialDiscontinuity,\n            periodStartUs,\n            periodEndUs);\n    mediaPeriods.add(mediaPeriod);\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    Assertions.checkState(mediaPeriods.remove(mediaPeriod));\n    mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);\n    if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) {\n      refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline);\n    }\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    clippingError = null;\n    clippingTimeline = null;\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {\n    if (clippingError != null) {\n      return;\n    }\n    refreshClippedTimeline(timeline);\n  }\n\n  private void refreshClippedTimeline(Timeline timeline) {\n    long windowStartUs;\n    long windowEndUs;\n    timeline.getWindow(/* windowIndex= */ 0, window);\n    long windowPositionInPeriodUs = window.getPositionInFirstPeriodUs();\n    if (clippingTimeline == null || mediaPeriods.isEmpty() || allowDynamicClippingUpdates) {\n      windowStartUs = startUs;\n      windowEndUs = endUs;\n      if (relativeToDefaultPosition) {\n        long windowDefaultPositionUs = window.getDefaultPositionUs();\n        windowStartUs += windowDefaultPositionUs;\n        windowEndUs += windowDefaultPositionUs;\n      }\n      periodStartUs = windowPositionInPeriodUs + windowStartUs;\n      periodEndUs =\n          endUs == C.TIME_END_OF_SOURCE\n              ? C.TIME_END_OF_SOURCE\n              : windowPositionInPeriodUs + windowEndUs;\n      int count = mediaPeriods.size();\n      for (int i = 0; i < count; i++) {\n        mediaPeriods.get(i).updateClipping(periodStartUs, periodEndUs);\n      }\n    } else {\n      // Keep window fixed at previous period position.\n      windowStartUs = periodStartUs - windowPositionInPeriodUs;\n      windowEndUs =\n          endUs == C.TIME_END_OF_SOURCE\n              ? C.TIME_END_OF_SOURCE\n              : periodEndUs - windowPositionInPeriodUs;\n    }\n    try {\n      clippingTimeline = new ClippingTimeline(timeline, windowStartUs, windowEndUs);\n    } catch (IllegalClippingException e) {\n      clippingError = e;\n      return;\n    }\n    refreshSourceInfo(clippingTimeline);\n  }\n\n  @Override\n  protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) {\n    if (mediaTimeMs == C.TIME_UNSET) {\n      return C.TIME_UNSET;\n    }\n    long startMs = C.usToMs(startUs);\n    long clippedTimeMs = Math.max(0, mediaTimeMs - startMs);\n    if (endUs != C.TIME_END_OF_SOURCE) {\n      clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs);\n    }\n    return clippedTimeMs;\n  }\n\n  /**\n   * Provides a clipped view of a specified timeline.\n   */\n  private static final class ClippingTimeline extends ForwardingTimeline {\n\n    private final long startUs;\n    private final long endUs;\n    private final long durationUs;\n    private final boolean isDynamic;\n\n    /**\n     * Creates a new clipping timeline that wraps the specified timeline.\n     *\n     * @param timeline The timeline to clip.\n     * @param startUs The number of microseconds to clip from the start of {@code timeline}.\n     * @param endUs The end position in microseconds for the clipped timeline relative to the start\n     *     of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.\n     * @throws IllegalClippingException If the timeline could not be clipped.\n     */\n    public ClippingTimeline(Timeline timeline, long startUs, long endUs)\n        throws IllegalClippingException {\n      super(timeline);\n      if (timeline.getPeriodCount() != 1) {\n        throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT);\n      }\n      Window window = timeline.getWindow(0, new Window());\n      startUs = Math.max(0, startUs);\n      long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs);\n      if (window.durationUs != C.TIME_UNSET) {\n        if (resolvedEndUs > window.durationUs) {\n          resolvedEndUs = window.durationUs;\n        }\n        if (startUs != 0 && !window.isSeekable) {\n          throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);\n        }\n        if (startUs > resolvedEndUs) {\n          throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END);\n        }\n      }\n      this.startUs = startUs;\n      this.endUs = resolvedEndUs;\n      durationUs = resolvedEndUs == C.TIME_UNSET ? C.TIME_UNSET : (resolvedEndUs - startUs);\n      isDynamic =\n          window.isDynamic\n              && (resolvedEndUs == C.TIME_UNSET\n                  || (window.durationUs != C.TIME_UNSET && resolvedEndUs == window.durationUs));\n    }\n\n    @Override\n    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n      timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0);\n      window.positionInFirstPeriodUs += startUs;\n      window.durationUs = durationUs;\n      window.isDynamic = isDynamic;\n      if (window.defaultPositionUs != C.TIME_UNSET) {\n        window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs);\n        window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs\n            : Math.min(window.defaultPositionUs, endUs);\n        window.defaultPositionUs -= startUs;\n      }\n      long startMs = C.usToMs(startUs);\n      if (window.presentationStartTimeMs != C.TIME_UNSET) {\n        window.presentationStartTimeMs += startMs;\n      }\n      if (window.windowStartTimeMs != C.TIME_UNSET) {\n        window.windowStartTimeMs += startMs;\n      }\n      return window;\n    }\n\n    @Override\n    public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n      timeline.getPeriod(/* periodIndex= */ 0, period, setIds);\n      long positionInClippedWindowUs = period.getPositionInWindowUs() - startUs;\n      long periodDurationUs =\n          durationUs == C.TIME_UNSET ? C.TIME_UNSET : durationUs - positionInClippedWindowUs;\n      return period.set(\n          period.id, period.uid, /* windowIndex= */ 0, periodDurationUs, positionInClippedWindowUs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Handler;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.HashMap;\n\n/**\n * Composite {@link MediaSource} consisting of multiple child sources.\n *\n * @param <T> The type of the id used to identify prepared child sources.\n */\npublic abstract class CompositeMediaSource<T> extends BaseMediaSource {\n\n  private final HashMap<T, MediaSourceAndListener> childSources;\n\n  @Nullable private Handler eventHandler;\n  @Nullable private TransferListener mediaTransferListener;\n\n  /** Creates composite media source without child sources. */\n  protected CompositeMediaSource() {\n    childSources = new HashMap<>();\n  }\n\n  @Override\n  @CallSuper\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\n    eventHandler = new Handler();\n  }\n\n  @Override\n  @CallSuper\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    for (MediaSourceAndListener childSource : childSources.values()) {\n      childSource.mediaSource.maybeThrowSourceInfoRefreshError();\n    }\n  }\n\n  @Override\n  @CallSuper\n  protected void enableInternal() {\n    for (MediaSourceAndListener childSource : childSources.values()) {\n      childSource.mediaSource.enable(childSource.caller);\n    }\n  }\n\n  @Override\n  @CallSuper\n  protected void disableInternal() {\n    for (MediaSourceAndListener childSource : childSources.values()) {\n      childSource.mediaSource.disable(childSource.caller);\n    }\n  }\n\n  @Override\n  @CallSuper\n  protected void releaseSourceInternal() {\n    for (MediaSourceAndListener childSource : childSources.values()) {\n      childSource.mediaSource.releaseSource(childSource.caller);\n      childSource.mediaSource.removeEventListener(childSource.eventListener);\n    }\n    childSources.clear();\n  }\n\n  /**\n   * Called when the source info of a child source has been refreshed.\n   *\n   * @param id The unique id used to prepare the child source.\n   * @param mediaSource The child source whose source info has been refreshed.\n   * @param timeline The timeline of the child source.\n   */\n  protected abstract void onChildSourceInfoRefreshed(\n      T id, MediaSource mediaSource, Timeline timeline);\n\n  /**\n   * Prepares a child source.\n   *\n   * <p>{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the\n   * child source updates its timeline with the same {@code id} passed to this method.\n   *\n   * <p>Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)}\n   * will be released in {@link #releaseSourceInternal()}.\n   *\n   * @param id A unique id to identify the child source preparation. Null is allowed as an id.\n   * @param mediaSource The child {@link MediaSource}.\n   */\n  protected final void prepareChildSource(final T id, MediaSource mediaSource) {\n    Assertions.checkArgument(!childSources.containsKey(id));\n    MediaSourceCaller caller =\n        (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline);\n    MediaSourceEventListener eventListener = new ForwardingEventListener(id);\n    childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener));\n    mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);\n    mediaSource.prepareSource(caller, mediaTransferListener);\n    if (!isEnabled()) {\n      mediaSource.disable(caller);\n    }\n  }\n\n  /**\n   * Enables a child source.\n   *\n   * @param id The unique id used to prepare the child source.\n   */\n  protected final void enableChildSource(final T id) {\n    MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id));\n    enabledChild.mediaSource.enable(enabledChild.caller);\n  }\n\n  /**\n   * Disables a child source.\n   *\n   * @param id The unique id used to prepare the child source.\n   */\n  protected final void disableChildSource(final T id) {\n    MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id));\n    disabledChild.mediaSource.disable(disabledChild.caller);\n  }\n\n  /**\n   * Releases a child source.\n   *\n   * @param id The unique id used to prepare the child source.\n   */\n  protected final void releaseChildSource(T id) {\n    MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id));\n    removedChild.mediaSource.releaseSource(removedChild.caller);\n    removedChild.mediaSource.removeEventListener(removedChild.eventListener);\n  }\n\n  /**\n   * Returns the window index in the composite source corresponding to the specified window index in\n   * a child source. The default implementation does not change the window index.\n   *\n   * @param id The unique id used to prepare the child source.\n   * @param windowIndex A window index of the child source.\n   * @return The corresponding window index in the composite source.\n   */\n  protected int getWindowIndexForChildWindowIndex(T id, int windowIndex) {\n    return windowIndex;\n  }\n\n  /**\n   * Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link\n   * MediaPeriodId} in a child source. The default implementation does not change the media period\n   * id.\n   *\n   * @param id The unique id used to prepare the child source.\n   * @param mediaPeriodId A {@link MediaPeriodId} of the child source.\n   * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no\n   *     corresponding media period id can be determined.\n   */\n  protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      T id, MediaPeriodId mediaPeriodId) {\n    return mediaPeriodId;\n  }\n\n  /**\n   * Returns the media time in the composite source corresponding to the specified media time in a\n   * child source. The default implementation does not change the media time.\n   *\n   * @param id The unique id used to prepare the child source.\n   * @param mediaTimeMs A media time of the child source, in milliseconds.\n   * @return The corresponding media time in the composite source, in milliseconds.\n   */\n  protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) {\n    return mediaTimeMs;\n  }\n\n  /**\n   * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and\n   * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given\n   * media period should be reported. The default implementation is to always report these events.\n   *\n   * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source.\n   * @return Whether create and release events for this media period should be reported.\n   */\n  protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) {\n    return true;\n  }\n\n  private static final class MediaSourceAndListener {\n\n    public final MediaSource mediaSource;\n    public final MediaSourceCaller caller;\n    public final MediaSourceEventListener eventListener;\n\n    public MediaSourceAndListener(\n        MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) {\n      this.mediaSource = mediaSource;\n      this.caller = caller;\n      this.eventListener = eventListener;\n    }\n  }\n\n  private final class ForwardingEventListener implements MediaSourceEventListener {\n\n    private final T id;\n    private EventDispatcher eventDispatcher;\n\n    public ForwardingEventListener(T id) {\n      this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);\n      this.id = id;\n    }\n\n    @Override\n    public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        if (shouldDispatchCreateOrReleaseEvent(\n            Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) {\n          eventDispatcher.mediaPeriodCreated();\n        }\n      }\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        if (shouldDispatchCreateOrReleaseEvent(\n            Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) {\n          eventDispatcher.mediaPeriodReleased();\n        }\n      }\n    }\n\n    @Override\n    public void onLoadStarted(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventData,\n        MediaLoadData mediaLoadData) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.loadStarted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));\n      }\n    }\n\n    @Override\n    public void onLoadCompleted(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventData,\n        MediaLoadData mediaLoadData) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.loadCompleted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));\n      }\n    }\n\n    @Override\n    public void onLoadCanceled(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventData,\n        MediaLoadData mediaLoadData) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.loadCanceled(loadEventData, maybeUpdateMediaLoadData(mediaLoadData));\n      }\n    }\n\n    @Override\n    public void onLoadError(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventData,\n        MediaLoadData mediaLoadData,\n        IOException error,\n        boolean wasCanceled) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.loadError(\n            loadEventData, maybeUpdateMediaLoadData(mediaLoadData), error, wasCanceled);\n      }\n    }\n\n    @Override\n    public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.readingStarted();\n      }\n    }\n\n    @Override\n    public void onUpstreamDiscarded(\n        int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.upstreamDiscarded(maybeUpdateMediaLoadData(mediaLoadData));\n      }\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(\n        int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.downstreamFormatChanged(maybeUpdateMediaLoadData(mediaLoadData));\n      }\n    }\n\n    /** Updates the event dispatcher and returns whether the event should be dispatched. */\n    private boolean maybeUpdateEventDispatcher(\n        int childWindowIndex, @Nullable MediaPeriodId childMediaPeriodId) {\n      MediaPeriodId mediaPeriodId = null;\n      if (childMediaPeriodId != null) {\n        mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);\n        if (mediaPeriodId == null) {\n          // Media period not found. Ignore event.\n          return false;\n        }\n      }\n      int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);\n      if (eventDispatcher.windowIndex != windowIndex\n          || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) {\n        eventDispatcher =\n            createEventDispatcher(windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0);\n      }\n      return true;\n    }\n\n    private MediaLoadData maybeUpdateMediaLoadData(MediaLoadData mediaLoadData) {\n      long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs);\n      long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs);\n      if (mediaStartTimeMs == mediaLoadData.mediaStartTimeMs\n          && mediaEndTimeMs == mediaLoadData.mediaEndTimeMs) {\n        return mediaLoadData;\n      }\n      return new MediaLoadData(\n          mediaLoadData.dataType,\n          mediaLoadData.trackType,\n          mediaLoadData.trackFormat,\n          mediaLoadData.trackSelectionReason,\n          mediaLoadData.trackSelectionData,\n          mediaStartTimeMs,\n          mediaEndTimeMs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\n\n/**\n * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.\n */\npublic class CompositeSequenceableLoader implements SequenceableLoader {\n\n  protected final SequenceableLoader[] loaders;\n\n  public CompositeSequenceableLoader(SequenceableLoader[] loaders) {\n    this.loaders = loaders;\n  }\n\n  @Override\n  public final long getBufferedPositionUs() {\n    long bufferedPositionUs = Long.MAX_VALUE;\n    for (SequenceableLoader loader : loaders) {\n      long loaderBufferedPositionUs = loader.getBufferedPositionUs();\n      if (loaderBufferedPositionUs != C.TIME_END_OF_SOURCE) {\n        bufferedPositionUs = Math.min(bufferedPositionUs, loaderBufferedPositionUs);\n      }\n    }\n    return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;\n  }\n\n  @Override\n  public final long getNextLoadPositionUs() {\n    long nextLoadPositionUs = Long.MAX_VALUE;\n    for (SequenceableLoader loader : loaders) {\n      long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();\n      if (loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE) {\n        nextLoadPositionUs = Math.min(nextLoadPositionUs, loaderNextLoadPositionUs);\n      }\n    }\n    return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs;\n  }\n\n  @Override\n  public final void reevaluateBuffer(long positionUs) {\n    for (SequenceableLoader loader : loaders) {\n      loader.reevaluateBuffer(positionUs);\n    }\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    boolean madeProgress = false;\n    boolean madeProgressThisIteration;\n    do {\n      madeProgressThisIteration = false;\n      long nextLoadPositionUs = getNextLoadPositionUs();\n      if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {\n        break;\n      }\n      for (SequenceableLoader loader : loaders) {\n        long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();\n        boolean isLoaderBehind =\n            loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE\n                && loaderNextLoadPositionUs <= positionUs;\n        if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) {\n          madeProgressThisIteration |= loader.continueLoading(positionUs);\n        }\n      }\n      madeProgress |= madeProgressThisIteration;\n    } while (madeProgressThisIteration);\n    return madeProgress;\n  }\n\n  @Override\n  public boolean isLoading() {\n    for (SequenceableLoader loader : loaders) {\n      if (loader.isLoading()) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\n/**\n * A factory to create composite {@link SequenceableLoader}s.\n */\npublic interface CompositeSequenceableLoaderFactory {\n\n  /**\n   * Creates a composite {@link SequenceableLoader}.\n   *\n   * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built.\n   * @return A composite {@link SequenceableLoader} that comprises the given loaders.\n   */\n  SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Handler;\nimport android.os.Message;\nimport androidx.annotation.GuardedBy;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;\nimport com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.IdentityHashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified\n * during playback. It is valid for the same {@link MediaSource} instance to be present more than\n * once in the concatenation. Access to this class is thread-safe.\n */\npublic final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {\n\n  private static final int MSG_ADD = 0;\n  private static final int MSG_REMOVE = 1;\n  private static final int MSG_MOVE = 2;\n  private static final int MSG_SET_SHUFFLE_ORDER = 3;\n  private static final int MSG_UPDATE_TIMELINE = 4;\n  private static final int MSG_ON_COMPLETION = 5;\n\n  // Accessed on any thread.\n  @GuardedBy(\"this\")\n  private final List<MediaSourceHolder> mediaSourcesPublic;\n\n  @GuardedBy(\"this\")\n  private final Set<HandlerAndRunnable> pendingOnCompletionActions;\n\n  @GuardedBy(\"this\")\n  @Nullable\n  private Handler playbackThreadHandler;\n\n  // Accessed on the playback thread only.\n  private final List<MediaSourceHolder> mediaSourceHolders;\n  private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;\n  private final Map<Object, MediaSourceHolder> mediaSourceByUid;\n  private final Set<MediaSourceHolder> enabledMediaSourceHolders;\n  private final boolean isAtomic;\n  private final boolean useLazyPreparation;\n\n  private boolean timelineUpdateScheduled;\n  private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;\n  private ShuffleOrder shuffleOrder;\n\n  /**\n   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same\n   *     {@link MediaSource} instance to be present more than once in the array.\n   */\n  public ConcatenatingMediaSource(MediaSource... mediaSources) {\n    this(/* isAtomic= */ false, mediaSources);\n  }\n\n  /**\n   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated\n   *     as a single item for repeating and shuffling.\n   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link\n   *     MediaSource} instance to be present more than once in the array.\n   */\n  public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) {\n    this(isAtomic, new DefaultShuffleOrder(0), mediaSources);\n  }\n\n  /**\n   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated\n   *     as a single item for repeating and shuffling.\n   * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.\n   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link\n   *     MediaSource} instance to be present more than once in the array.\n   */\n  public ConcatenatingMediaSource(\n      boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) {\n    this(isAtomic, /* useLazyPreparation= */ false, shuffleOrder, mediaSources);\n  }\n\n  /**\n   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated\n   *     as a single item for repeating and shuffling.\n   * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest\n   *     loads and other initial preparation steps happen immediately. If true, these initial\n   *     preparations are triggered only when the player starts buffering the media.\n   * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.\n   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link\n   *     MediaSource} instance to be present more than once in the array.\n   */\n  @SuppressWarnings(\"initialization\")\n  public ConcatenatingMediaSource(\n      boolean isAtomic,\n      boolean useLazyPreparation,\n      ShuffleOrder shuffleOrder,\n      MediaSource... mediaSources) {\n    for (MediaSource mediaSource : mediaSources) {\n      Assertions.checkNotNull(mediaSource);\n    }\n    this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;\n    this.mediaSourceByMediaPeriod = new IdentityHashMap<>();\n    this.mediaSourceByUid = new HashMap<>();\n    this.mediaSourcesPublic = new ArrayList<>();\n    this.mediaSourceHolders = new ArrayList<>();\n    this.nextTimelineUpdateOnCompletionActions = new HashSet<>();\n    this.pendingOnCompletionActions = new HashSet<>();\n    this.enabledMediaSourceHolders = new HashSet<>();\n    this.isAtomic = isAtomic;\n    this.useLazyPreparation = useLazyPreparation;\n    addMediaSources(Arrays.asList(mediaSources));\n  }\n\n  /**\n   * Appends a {@link MediaSource} to the playlist.\n   *\n   * @param mediaSource The {@link MediaSource} to be added to the list.\n   */\n  public synchronized void addMediaSource(MediaSource mediaSource) {\n    addMediaSource(mediaSourcesPublic.size(), mediaSource);\n  }\n\n  /**\n   * Appends a {@link MediaSource} to the playlist and executes a custom action on completion.\n   *\n   * @param mediaSource The {@link MediaSource} to be added to the list.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     source has been added to the playlist.\n   */\n  public synchronized void addMediaSource(\n      MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {\n    addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);\n  }\n\n  /**\n   * Adds a {@link MediaSource} to the playlist.\n   *\n   * @param index The index at which the new {@link MediaSource} will be inserted. This index must\n   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param mediaSource The {@link MediaSource} to be added to the list.\n   */\n  public synchronized void addMediaSource(int index, MediaSource mediaSource) {\n    addPublicMediaSources(\n        index,\n        Collections.singletonList(mediaSource),\n        /* handler= */ null,\n        /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Adds a {@link MediaSource} to the playlist and executes a custom action on completion.\n   *\n   * @param index The index at which the new {@link MediaSource} will be inserted. This index must\n   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param mediaSource The {@link MediaSource} to be added to the list.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     source has been added to the playlist.\n   */\n  public synchronized void addMediaSource(\n      int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {\n    addPublicMediaSources(\n        index, Collections.singletonList(mediaSource), handler, onCompletionAction);\n  }\n\n  /**\n   * Appends multiple {@link MediaSource}s to the playlist.\n   *\n   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media\n   *     sources are added in the order in which they appear in this collection.\n   */\n  public synchronized void addMediaSources(Collection<MediaSource> mediaSources) {\n    addPublicMediaSources(\n        mediaSourcesPublic.size(),\n        mediaSources,\n        /* handler= */ null,\n        /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on\n   * completion.\n   *\n   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media\n   *     sources are added in the order in which they appear in this collection.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     sources have been added to the playlist.\n   */\n  public synchronized void addMediaSources(\n      Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {\n    addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);\n  }\n\n  /**\n   * Adds multiple {@link MediaSource}s to the playlist.\n   *\n   * @param index The index at which the new {@link MediaSource}s will be inserted. This index must\n   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media\n   *     sources are added in the order in which they appear in this collection.\n   */\n  public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {\n    addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.\n   *\n   * @param index The index at which the new {@link MediaSource}s will be inserted. This index must\n   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media\n   *     sources are added in the order in which they appear in this collection.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     sources have been added to the playlist.\n   */\n  public synchronized void addMediaSources(\n      int index,\n      Collection<MediaSource> mediaSources,\n      Handler handler,\n      Runnable onCompletionAction) {\n    addPublicMediaSources(index, mediaSources, handler, onCompletionAction);\n  }\n\n  /**\n   * Removes a {@link MediaSource} from the playlist.\n   *\n   * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,\n   * int)} instead.\n   *\n   * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link\n   * #removeMediaSourceRange(int, int)} instead.\n   *\n   * @param index The index at which the media source will be removed. This index must be in the\n   *     range of 0 &lt;= index &lt; {@link #getSize()}.\n   * @return The removed {@link MediaSource}.\n   */\n  public synchronized MediaSource removeMediaSource(int index) {\n    MediaSource removedMediaSource = getMediaSource(index);\n    removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);\n    return removedMediaSource;\n  }\n\n  /**\n   * Removes a {@link MediaSource} from the playlist and executes a custom action on completion.\n   *\n   * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,\n   * int, Handler, Runnable)} instead.\n   *\n   * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link\n   * #removeMediaSourceRange(int, int, Handler, Runnable)} instead.\n   *\n   * @param index The index at which the media source will be removed. This index must be in the\n   *     range of 0 &lt;= index &lt; {@link #getSize()}.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     source has been removed from the playlist.\n   * @return The removed {@link MediaSource}.\n   */\n  public synchronized MediaSource removeMediaSource(\n      int index, Handler handler, Runnable onCompletionAction) {\n    MediaSource removedMediaSource = getMediaSource(index);\n    removePublicMediaSources(index, index + 1, handler, onCompletionAction);\n    return removedMediaSource;\n  }\n\n  /**\n   * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index\n   * (included) and a final index (excluded).\n   *\n   * <p>Note: when specified range is empty, no actual media source is removed and no exception is\n   * thrown.\n   *\n   * @param fromIndex The initial range index, pointing to the first media source that will be\n   *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param toIndex The final range index, pointing to the first media source that will be left\n   *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} &lt; 0,\n   *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}\n   */\n  public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {\n    removePublicMediaSources(\n        fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index\n   * (included) and a final index (excluded), and executes a custom action on completion.\n   *\n   * <p>Note: when specified range is empty, no actual media source is removed and no exception is\n   * thrown.\n   *\n   * @param fromIndex The initial range index, pointing to the first media source that will be\n   *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param toIndex The final range index, pointing to the first media source that will be left\n   *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     source range has been removed from the playlist.\n   * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,\n   *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}\n   */\n  public synchronized void removeMediaSourceRange(\n      int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {\n    removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);\n  }\n\n  /**\n   * Moves an existing {@link MediaSource} within the playlist.\n   *\n   * @param currentIndex The current index of the media source in the playlist. This index must be\n   *     in the range of 0 &lt;= index &lt; {@link #getSize()}.\n   * @param newIndex The target index of the media source in the playlist. This index must be in the\n   *     range of 0 &lt;= index &lt; {@link #getSize()}.\n   */\n  public synchronized void moveMediaSource(int currentIndex, int newIndex) {\n    movePublicMediaSource(\n        currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Moves an existing {@link MediaSource} within the playlist and executes a custom action on\n   * completion.\n   *\n   * @param currentIndex The current index of the media source in the playlist. This index must be\n   *     in the range of 0 &lt;= index &lt; {@link #getSize()}.\n   * @param newIndex The target index of the media source in the playlist. This index must be in the\n   *     range of 0 &lt;= index &lt; {@link #getSize()}.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media\n   *     source has been moved.\n   */\n  public synchronized void moveMediaSource(\n      int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {\n    movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);\n  }\n\n  /** Clears the playlist. */\n  public synchronized void clear() {\n    removeMediaSourceRange(0, getSize());\n  }\n\n  /**\n   * Clears the playlist and executes a custom action on completion.\n   *\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist\n   *     has been cleared.\n   */\n  public synchronized void clear(Handler handler, Runnable onCompletionAction) {\n    removeMediaSourceRange(0, getSize(), handler, onCompletionAction);\n  }\n\n  /** Returns the number of media sources in the playlist. */\n  public synchronized int getSize() {\n    return mediaSourcesPublic.size();\n  }\n\n  /**\n   * Returns the {@link MediaSource} at a specified index.\n   *\n   * @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.\n   * @return The {@link MediaSource} at this index.\n   */\n  public synchronized MediaSource getMediaSource(int index) {\n    return mediaSourcesPublic.get(index).mediaSource;\n  }\n\n  /**\n   * Sets a new shuffle order to use when shuffling the child media sources.\n   *\n   * @param shuffleOrder A {@link ShuffleOrder}.\n   */\n  public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {\n    setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);\n  }\n\n  /**\n   * Sets a new shuffle order to use when shuffling the child media sources.\n   *\n   * @param shuffleOrder A {@link ShuffleOrder}.\n   * @param handler The {@link Handler} to run {@code onCompletionAction}.\n   * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle\n   *     order has been changed.\n   */\n  public synchronized void setShuffleOrder(\n      ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {\n    setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);\n  }\n\n  // CompositeMediaSource implementation.\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return null;\n  }\n\n  @Override\n  protected synchronized void prepareSourceInternal(\n      @Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);\n    if (mediaSourcesPublic.isEmpty()) {\n      updateTimelineAndScheduleOnCompletionActions();\n    } else {\n      shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());\n      addMediaSourcesInternal(0, mediaSourcesPublic);\n      scheduleTimelineUpdate();\n    }\n  }\n\n  @SuppressWarnings(\"MissingSuperCall\")\n  @Override\n  protected void enableInternal() {\n    // Suppress enabling all child sources here as they can be lazily enabled when creating periods.\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);\n    MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));\n    MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);\n    if (holder == null) {\n      // Stale event. The media source has already been removed.\n      holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation);\n      holder.isRemoved = true;\n      prepareChildSource(holder, holder.mediaSource);\n    }\n    enableMediaSource(holder);\n    holder.activeMediaPeriodIds.add(childMediaPeriodId);\n    MediaPeriod mediaPeriod =\n        holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);\n    mediaSourceByMediaPeriod.put(mediaPeriod, holder);\n    disableUnusedMediaSources();\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    MediaSourceHolder holder =\n        Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));\n    holder.mediaSource.releasePeriod(mediaPeriod);\n    holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);\n    if (!mediaSourceByMediaPeriod.isEmpty()) {\n      disableUnusedMediaSources();\n    }\n    maybeReleaseChildSource(holder);\n  }\n\n  @Override\n  protected void disableInternal() {\n    super.disableInternal();\n    enabledMediaSourceHolders.clear();\n  }\n\n  @Override\n  protected synchronized void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    mediaSourceHolders.clear();\n    enabledMediaSourceHolders.clear();\n    mediaSourceByUid.clear();\n    shuffleOrder = shuffleOrder.cloneAndClear();\n    if (playbackThreadHandler != null) {\n      playbackThreadHandler.removeCallbacksAndMessages(null);\n      playbackThreadHandler = null;\n    }\n    timelineUpdateScheduled = false;\n    nextTimelineUpdateOnCompletionActions.clear();\n    dispatchOnCompletionActions(pendingOnCompletionActions);\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) {\n    updateMediaSourceInternal(mediaSourceHolder, timeline);\n  }\n\n  @Override\n  @Nullable\n  protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {\n    for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {\n      // Ensure the reported media period id has the same window sequence number as the one created\n      // by this media source. Otherwise it does not belong to this child source.\n      if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber\n          == mediaPeriodId.windowSequenceNumber) {\n        Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);\n        return mediaPeriodId.copyWithPeriodUid(periodUid);\n      }\n    }\n    return null;\n  }\n\n  @Override\n  protected int getWindowIndexForChildWindowIndex(\n      MediaSourceHolder mediaSourceHolder, int windowIndex) {\n    return windowIndex + mediaSourceHolder.firstWindowIndexInChild;\n  }\n\n  // Internal methods. Called from any thread.\n\n  @GuardedBy(\"this\")\n  private void addPublicMediaSources(\n      int index,\n      Collection<MediaSource> mediaSources,\n      @Nullable Handler handler,\n      @Nullable Runnable onCompletionAction) {\n    Assertions.checkArgument((handler == null) == (onCompletionAction == null));\n    Handler playbackThreadHandler = this.playbackThreadHandler;\n    for (MediaSource mediaSource : mediaSources) {\n      Assertions.checkNotNull(mediaSource);\n    }\n    List<MediaSourceHolder> mediaSourceHolders = new ArrayList<>(mediaSources.size());\n    for (MediaSource mediaSource : mediaSources) {\n      mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation));\n    }\n    mediaSourcesPublic.addAll(index, mediaSourceHolders);\n    if (playbackThreadHandler != null && !mediaSources.isEmpty()) {\n      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);\n      playbackThreadHandler\n          .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))\n          .sendToTarget();\n    } else if (onCompletionAction != null && handler != null) {\n      handler.post(onCompletionAction);\n    }\n  }\n\n  @GuardedBy(\"this\")\n  private void removePublicMediaSources(\n      int fromIndex,\n      int toIndex,\n      @Nullable Handler handler,\n      @Nullable Runnable onCompletionAction) {\n    Assertions.checkArgument((handler == null) == (onCompletionAction == null));\n    Handler playbackThreadHandler = this.playbackThreadHandler;\n    Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);\n    if (playbackThreadHandler != null) {\n      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);\n      playbackThreadHandler\n          .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))\n          .sendToTarget();\n    } else if (onCompletionAction != null && handler != null) {\n      handler.post(onCompletionAction);\n    }\n  }\n\n  @GuardedBy(\"this\")\n  private void movePublicMediaSource(\n      int currentIndex,\n      int newIndex,\n      @Nullable Handler handler,\n      @Nullable Runnable onCompletionAction) {\n    Assertions.checkArgument((handler == null) == (onCompletionAction == null));\n    Handler playbackThreadHandler = this.playbackThreadHandler;\n    mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));\n    if (playbackThreadHandler != null) {\n      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);\n      playbackThreadHandler\n          .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))\n          .sendToTarget();\n    } else if (onCompletionAction != null && handler != null) {\n      handler.post(onCompletionAction);\n    }\n  }\n\n  @GuardedBy(\"this\")\n  private void setPublicShuffleOrder(\n      ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {\n    Assertions.checkArgument((handler == null) == (onCompletionAction == null));\n    Handler playbackThreadHandler = this.playbackThreadHandler;\n    if (playbackThreadHandler != null) {\n      int size = getSize();\n      if (shuffleOrder.getLength() != size) {\n        shuffleOrder =\n            shuffleOrder\n                .cloneAndClear()\n                .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);\n      }\n      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);\n      playbackThreadHandler\n          .obtainMessage(\n              MSG_SET_SHUFFLE_ORDER,\n              new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))\n          .sendToTarget();\n    } else {\n      this.shuffleOrder =\n          shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;\n      if (onCompletionAction != null && handler != null) {\n        handler.post(onCompletionAction);\n      }\n    }\n  }\n\n  @GuardedBy(\"this\")\n  @Nullable\n  private HandlerAndRunnable createOnCompletionAction(\n      @Nullable Handler handler, @Nullable Runnable runnable) {\n    if (handler == null || runnable == null) {\n      return null;\n    }\n    HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);\n    pendingOnCompletionActions.add(handlerAndRunnable);\n    return handlerAndRunnable;\n  }\n\n  // Internal methods. Called on the playback thread.\n\n  @SuppressWarnings(\"unchecked\")\n  private boolean handleMessage(Message msg) {\n    switch (msg.what) {\n      case MSG_ADD:\n        MessageData<Collection<MediaSourceHolder>> addMessage =\n            (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);\n        shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());\n        addMediaSourcesInternal(addMessage.index, addMessage.customData);\n        scheduleTimelineUpdate(addMessage.onCompletionAction);\n        break;\n      case MSG_REMOVE:\n        MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);\n        int fromIndex = removeMessage.index;\n        int toIndex = removeMessage.customData;\n        if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {\n          shuffleOrder = shuffleOrder.cloneAndClear();\n        } else {\n          shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex);\n        }\n        for (int index = toIndex - 1; index >= fromIndex; index--) {\n          removeMediaSourceInternal(index);\n        }\n        scheduleTimelineUpdate(removeMessage.onCompletionAction);\n        break;\n      case MSG_MOVE:\n        MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);\n        shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);\n        shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);\n        moveMediaSourceInternal(moveMessage.index, moveMessage.customData);\n        scheduleTimelineUpdate(moveMessage.onCompletionAction);\n        break;\n      case MSG_SET_SHUFFLE_ORDER:\n        MessageData<ShuffleOrder> shuffleOrderMessage =\n            (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);\n        shuffleOrder = shuffleOrderMessage.customData;\n        scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);\n        break;\n      case MSG_UPDATE_TIMELINE:\n        updateTimelineAndScheduleOnCompletionActions();\n        break;\n      case MSG_ON_COMPLETION:\n        Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);\n        dispatchOnCompletionActions(actions);\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n    return true;\n  }\n\n  private void scheduleTimelineUpdate() {\n    scheduleTimelineUpdate(/* onCompletionAction= */ null);\n  }\n\n  private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {\n    if (!timelineUpdateScheduled) {\n      getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();\n      timelineUpdateScheduled = true;\n    }\n    if (onCompletionAction != null) {\n      nextTimelineUpdateOnCompletionActions.add(onCompletionAction);\n    }\n  }\n\n  private void updateTimelineAndScheduleOnCompletionActions() {\n    timelineUpdateScheduled = false;\n    Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;\n    nextTimelineUpdateOnCompletionActions = new HashSet<>();\n    refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic));\n    getPlaybackThreadHandlerOnPlaybackThread()\n        .obtainMessage(MSG_ON_COMPLETION, onCompletionActions)\n        .sendToTarget();\n  }\n\n  @SuppressWarnings(\"GuardedBy\")\n  private Handler getPlaybackThreadHandlerOnPlaybackThread() {\n    // Write access to this value happens on the playback thread only, so playback thread reads\n    // don't need to be synchronized.\n    return Assertions.checkNotNull(playbackThreadHandler);\n  }\n\n  private synchronized void dispatchOnCompletionActions(\n      Set<HandlerAndRunnable> onCompletionActions) {\n    for (HandlerAndRunnable pendingAction : onCompletionActions) {\n      pendingAction.dispatch();\n    }\n    pendingOnCompletionActions.removeAll(onCompletionActions);\n  }\n\n  private void addMediaSourcesInternal(\n      int index, Collection<MediaSourceHolder> mediaSourceHolders) {\n    for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {\n      addMediaSourceInternal(index++, mediaSourceHolder);\n    }\n  }\n\n  private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) {\n    if (newIndex > 0) {\n      MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);\n      Timeline previousTimeline = previousHolder.mediaSource.getTimeline();\n      newMediaSourceHolder.reset(\n          newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount());\n    } else {\n      newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0);\n    }\n    Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline();\n    correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount());\n    mediaSourceHolders.add(newIndex, newMediaSourceHolder);\n    mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder);\n    prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);\n    if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) {\n      enabledMediaSourceHolders.add(newMediaSourceHolder);\n    } else {\n      disableChildSource(newMediaSourceHolder);\n    }\n  }\n\n  private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {\n    if (mediaSourceHolder == null) {\n      throw new IllegalArgumentException();\n    }\n    if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) {\n      MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1);\n      int windowOffsetUpdate =\n          timeline.getWindowCount()\n              - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild);\n      if (windowOffsetUpdate != 0) {\n        correctOffsets(\n            mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate);\n      }\n    }\n    scheduleTimelineUpdate();\n  }\n\n  private void removeMediaSourceInternal(int index) {\n    MediaSourceHolder holder = mediaSourceHolders.remove(index);\n    mediaSourceByUid.remove(holder.uid);\n    Timeline oldTimeline = holder.mediaSource.getTimeline();\n    correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount());\n    holder.isRemoved = true;\n    maybeReleaseChildSource(holder);\n  }\n\n  private void moveMediaSourceInternal(int currentIndex, int newIndex) {\n    int startIndex = Math.min(currentIndex, newIndex);\n    int endIndex = Math.max(currentIndex, newIndex);\n    int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;\n    mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));\n    for (int i = startIndex; i <= endIndex; i++) {\n      MediaSourceHolder holder = mediaSourceHolders.get(i);\n      holder.childIndex = i;\n      holder.firstWindowIndexInChild = windowOffset;\n      windowOffset += holder.mediaSource.getTimeline().getWindowCount();\n    }\n  }\n\n  private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) {\n    // TODO: Replace window index with uid in reporting to get rid of this inefficient method and\n    // the childIndex and firstWindowIndexInChild variables.\n    for (int i = startIndex; i < mediaSourceHolders.size(); i++) {\n      MediaSourceHolder holder = mediaSourceHolders.get(i);\n      holder.childIndex += childIndexUpdate;\n      holder.firstWindowIndexInChild += windowOffsetUpdate;\n    }\n  }\n\n  private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {\n    // Release if the source has been removed from the playlist and no periods are still active.\n    if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {\n      enabledMediaSourceHolders.remove(mediaSourceHolder);\n      releaseChildSource(mediaSourceHolder);\n    }\n  }\n\n  private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {\n    enabledMediaSourceHolders.add(mediaSourceHolder);\n    enableChildSource(mediaSourceHolder);\n  }\n\n  private void disableUnusedMediaSources() {\n    Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();\n    while (iterator.hasNext()) {\n      MediaSourceHolder holder = iterator.next();\n      if (holder.activeMediaPeriodIds.isEmpty()) {\n        disableChildSource(holder);\n        iterator.remove();\n      }\n    }\n  }\n\n  /** Return uid of media source holder from period uid of concatenated source. */\n  private static Object getMediaSourceHolderUid(Object periodUid) {\n    return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);\n  }\n\n  /** Return uid of child period from period uid of concatenated source. */\n  private static Object getChildPeriodUid(Object periodUid) {\n    return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);\n  }\n\n  private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {\n    return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid);\n  }\n\n  /** Data class to hold playlist media sources together with meta data needed to process them. */\n  /* package */ static final class MediaSourceHolder {\n\n    public final MaskingMediaSource mediaSource;\n    public final Object uid;\n    public final List<MediaPeriodId> activeMediaPeriodIds;\n\n    public int childIndex;\n    public int firstWindowIndexInChild;\n    public boolean isRemoved;\n\n    public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {\n      this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);\n      this.activeMediaPeriodIds = new ArrayList<>();\n      this.uid = new Object();\n    }\n\n    public void reset(int childIndex, int firstWindowIndexInChild) {\n      this.childIndex = childIndex;\n      this.firstWindowIndexInChild = firstWindowIndexInChild;\n      this.isRemoved = false;\n      this.activeMediaPeriodIds.clear();\n    }\n  }\n\n  /** Message used to post actions from app thread to playback thread. */\n  private static final class MessageData<T> {\n\n    public final int index;\n    public final T customData;\n    @Nullable public final HandlerAndRunnable onCompletionAction;\n\n    public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {\n      this.index = index;\n      this.customData = customData;\n      this.onCompletionAction = onCompletionAction;\n    }\n  }\n\n  /** Timeline exposing concatenated timelines of playlist media sources. */\n  private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {\n\n    private final int windowCount;\n    private final int periodCount;\n    private final int[] firstPeriodInChildIndices;\n    private final int[] firstWindowInChildIndices;\n    private final Timeline[] timelines;\n    private final Object[] uids;\n    private final HashMap<Object, Integer> childIndexByUid;\n\n    public ConcatenatedTimeline(\n        Collection<MediaSourceHolder> mediaSourceHolders,\n        ShuffleOrder shuffleOrder,\n        boolean isAtomic) {\n      super(isAtomic, shuffleOrder);\n      int childCount = mediaSourceHolders.size();\n      firstPeriodInChildIndices = new int[childCount];\n      firstWindowInChildIndices = new int[childCount];\n      timelines = new Timeline[childCount];\n      uids = new Object[childCount];\n      childIndexByUid = new HashMap<>();\n      int index = 0;\n      int windowCount = 0;\n      int periodCount = 0;\n      for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {\n        timelines[index] = mediaSourceHolder.mediaSource.getTimeline();\n        firstWindowInChildIndices[index] = windowCount;\n        firstPeriodInChildIndices[index] = periodCount;\n        windowCount += timelines[index].getWindowCount();\n        periodCount += timelines[index].getPeriodCount();\n        uids[index] = mediaSourceHolder.uid;\n        childIndexByUid.put(uids[index], index++);\n      }\n      this.windowCount = windowCount;\n      this.periodCount = periodCount;\n    }\n\n    @Override\n    protected int getChildIndexByPeriodIndex(int periodIndex) {\n      return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);\n    }\n\n    @Override\n    protected int getChildIndexByWindowIndex(int windowIndex) {\n      return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);\n    }\n\n    @Override\n    protected int getChildIndexByChildUid(Object childUid) {\n      Integer index = childIndexByUid.get(childUid);\n      return index == null ? C.INDEX_UNSET : index;\n    }\n\n    @Override\n    protected Timeline getTimelineByChildIndex(int childIndex) {\n      return timelines[childIndex];\n    }\n\n    @Override\n    protected int getFirstPeriodIndexByChildIndex(int childIndex) {\n      return firstPeriodInChildIndices[childIndex];\n    }\n\n    @Override\n    protected int getFirstWindowIndexByChildIndex(int childIndex) {\n      return firstWindowInChildIndices[childIndex];\n    }\n\n    @Override\n    protected Object getChildUidByChildIndex(int childIndex) {\n      return uids[childIndex];\n    }\n\n    @Override\n    public int getWindowCount() {\n      return windowCount;\n    }\n\n    @Override\n    public int getPeriodCount() {\n      return periodCount;\n    }\n  }\n\n  /** Dummy media source which does nothing and does not support creating periods. */\n  private static final class DummyMediaSource extends BaseMediaSource {\n\n    @Override\n    protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n      // Do nothing.\n    }\n\n    @Override\n    @Nullable\n    public Object getTag() {\n      return null;\n    }\n\n    @Override\n    protected void releaseSourceInternal() {\n      // Do nothing.\n    }\n\n    @Override\n    public void maybeThrowSourceInfoRefreshError() throws IOException {\n      // Do nothing.\n    }\n\n    @Override\n    public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void releasePeriod(MediaPeriod mediaPeriod) {\n      // Do nothing.\n    }\n  }\n\n  private static final class HandlerAndRunnable {\n\n    private final Handler handler;\n    private final Runnable runnable;\n\n    public HandlerAndRunnable(Handler handler, Runnable runnable) {\n      this.handler = handler;\n      this.runnable = runnable;\n    }\n\n    public void dispatch() {\n      handler.post(runnable);\n    }\n  }\n}\n\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\n/**\n * Default implementation of {@link CompositeSequenceableLoaderFactory}.\n */\npublic final class DefaultCompositeSequenceableLoaderFactory\n    implements CompositeSequenceableLoaderFactory {\n\n  @Override\n  public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) {\n    return new CompositeSequenceableLoader(loaders);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\n/**\n * @deprecated Use {@link MediaSourceEventListener} interface directly for selective overrides as\n *     all methods are implemented as no-op default methods.\n */\n@Deprecated\npublic abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport java.io.IOException;\n\n/**\n * An empty {@link SampleStream}.\n */\npublic final class EmptySampleStream implements SampleStream {\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n      boolean formatRequired) {\n    buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n    return C.RESULT_BUFFER_READ;\n  }\n\n  @Override\n  public int skipData(long positionUs) {\n    return 0;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\n\n/** @deprecated Use {@link ProgressiveMediaSource} instead. */\n@Deprecated\n@SuppressWarnings(\"deprecation\")\npublic final class ExtractorMediaSource extends CompositeMediaSource<Void> {\n\n  /** @deprecated Use {@link MediaSourceEventListener} instead. */\n  @Deprecated\n  public interface EventListener {\n\n    /**\n     * Called when an error occurs loading media data.\n     * <p>\n     * This method being called does not indicate that playback has failed, or that it will fail.\n     * The player may be able to recover from the error and continue. Hence applications should\n     * <em>not</em> implement this method to display a user visible error or initiate an application\n     * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement\n     * such behavior). This method is called to provide the application with an opportunity to log\n     * the error if it wishes to do so.\n     *\n     * @param error The load error.\n     */\n    void onLoadError(IOException error);\n\n  }\n\n  /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */\n  @Deprecated\n  public static final class Factory implements MediaSourceFactory {\n\n    private final DataSource.Factory dataSourceFactory;\n\n    @Nullable private ExtractorsFactory extractorsFactory;\n    @Nullable private String customCacheKey;\n    @Nullable private Object tag;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private int continueLoadingCheckIntervalBytes;\n    private boolean isCreateCalled;\n\n    /**\n     * Creates a new factory for {@link ExtractorMediaSource}s.\n     *\n     * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this.dataSourceFactory = dataSourceFactory;\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;\n    }\n\n    /**\n     * Sets the factory for {@link Extractor}s to process the media stream. The default value is an\n     * instance of {@link DefaultExtractorsFactory}.\n     *\n     * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the\n     *     possible formats are known, pass a factory that instantiates extractors for those\n     *     formats.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.extractorsFactory = extractorsFactory;\n      return this;\n    }\n\n    /**\n     * Sets the custom key that uniquely identifies the original stream. Used for cache indexing.\n     * The default value is {@code null}.\n     *\n     * @param customCacheKey A custom key that uniquely identifies the original stream. Used for\n     *     cache indexing.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setCustomCacheKey(String customCacheKey) {\n      Assertions.checkState(!isCreateCalled);\n      this.customCacheKey = customCacheKey;\n      return this;\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link\n     * com.google.android.exoplayer2.Timeline} of the source as {@link\n     * com.google.android.exoplayer2.Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTag(Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the minimum number of times to retry if a loading error occurs. See {@link\n     * #setLoadErrorHandlingPolicy} for the default value.\n     *\n     * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\n     * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\n     * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\n     *\n     * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\n     */\n    @Deprecated\n    public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\n      return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /**\n     * Sets the number of bytes that should be loaded between each invocation of {@link\n     * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is\n     * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.\n     *\n     * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between\n     *     each invocation of {@link\n     *     MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {\n      Assertions.checkState(!isCreateCalled);\n      this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;\n      return this;\n    }\n\n    /**\n     * Returns a new {@link ExtractorMediaSource} using the current parameters.\n     *\n     * @param uri The {@link Uri}.\n     * @return The new {@link ExtractorMediaSource}.\n     */\n    @Override\n    public ExtractorMediaSource createMediaSource(Uri uri) {\n      isCreateCalled = true;\n      if (extractorsFactory == null) {\n        extractorsFactory = new DefaultExtractorsFactory();\n      }\n      return new ExtractorMediaSource(\n          uri,\n          dataSourceFactory,\n          extractorsFactory,\n          loadErrorHandlingPolicy,\n          customCacheKey,\n          continueLoadingCheckIntervalBytes,\n          tag);\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,\n     *     MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public ExtractorMediaSource createMediaSource(\n        Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) {\n      ExtractorMediaSource mediaSource = createMediaSource(uri);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    @Override\n    public int[] getSupportedTypes() {\n      return new int[] {C.TYPE_OTHER};\n    }\n  }\n\n  /**\n   * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead.\n   */\n  @Deprecated\n  public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =\n      ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;\n\n  private final ProgressiveMediaSource progressiveMediaSource;\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n   * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the\n   *     possible formats are known, pass a factory that instantiates extractors for those formats.\n   *     Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public ExtractorMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      ExtractorsFactory extractorsFactory,\n      @Nullable Handler eventHandler,\n      @Nullable EventListener eventListener) {\n    this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);\n  }\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n   * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the\n   *     possible formats are known, pass a factory that instantiates extractors for those formats.\n   *     Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache\n   *     indexing. May be null.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public ExtractorMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      ExtractorsFactory extractorsFactory,\n      @Nullable Handler eventHandler,\n      @Nullable EventListener eventListener,\n      @Nullable String customCacheKey) {\n    this(\n        uri,\n        dataSourceFactory,\n        extractorsFactory,\n        eventHandler,\n        eventListener,\n        customCacheKey,\n        DEFAULT_LOADING_CHECK_INTERVAL_BYTES);\n  }\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n   * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the\n   *     possible formats are known, pass a factory that instantiates extractors for those formats.\n   *     Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache\n   *     indexing. May be null.\n   * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each\n   *     invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public ExtractorMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      ExtractorsFactory extractorsFactory,\n      @Nullable Handler eventHandler,\n      @Nullable EventListener eventListener,\n      @Nullable String customCacheKey,\n      int continueLoadingCheckIntervalBytes) {\n    this(\n        uri,\n        dataSourceFactory,\n        extractorsFactory,\n        new DefaultLoadErrorHandlingPolicy(),\n        customCacheKey,\n        continueLoadingCheckIntervalBytes,\n        /* tag= */ null);\n    if (eventListener != null && eventHandler != null) {\n      addEventListener(eventHandler, new EventListenerWrapper(eventListener));\n    }\n  }\n\n  private ExtractorMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      ExtractorsFactory extractorsFactory,\n      LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,\n      @Nullable String customCacheKey,\n      int continueLoadingCheckIntervalBytes,\n      @Nullable Object tag) {\n    progressiveMediaSource =\n        new ProgressiveMediaSource(\n            uri,\n            dataSourceFactory,\n            extractorsFactory,\n            DrmSessionManager.getDummyDrmSessionManager(),\n            loadableLoadErrorHandlingPolicy,\n            customCacheKey,\n            continueLoadingCheckIntervalBytes,\n            tag);\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return progressiveMediaSource.getTag();\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    prepareChildSource(/* id= */ null, progressiveMediaSource);\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      @Nullable Void id, MediaSource mediaSource, Timeline timeline) {\n    refreshSourceInfo(timeline);\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    return progressiveMediaSource.createPeriod(id, allocator, startPositionUs);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    progressiveMediaSource.releasePeriod(mediaPeriod);\n  }\n\n  @Deprecated\n  private static final class EventListenerWrapper implements MediaSourceEventListener {\n\n    private final EventListener eventListener;\n\n    public EventListenerWrapper(EventListener eventListener) {\n      this.eventListener = Assertions.checkNotNull(eventListener);\n    }\n\n    @Override\n    public void onLoadError(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData,\n        IOException error,\n        boolean wasCanceled) {\n      eventListener.onLoadError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\n\n/**\n * An overridable {@link Timeline} implementation forwarding all methods to another timeline.\n */\npublic abstract class ForwardingTimeline extends Timeline {\n\n  protected final Timeline timeline;\n\n  public ForwardingTimeline(Timeline timeline) {\n    this.timeline = timeline;\n  }\n\n  @Override\n  public int getWindowCount() {\n    return timeline.getWindowCount();\n  }\n\n  @Override\n  public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled) {\n    return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);\n  }\n\n  @Override\n  public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled) {\n    return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);\n  }\n\n  @Override\n  public int getLastWindowIndex(boolean shuffleModeEnabled) {\n    return timeline.getLastWindowIndex(shuffleModeEnabled);\n  }\n\n  @Override\n  public int getFirstWindowIndex(boolean shuffleModeEnabled) {\n    return timeline.getFirstWindowIndex(shuffleModeEnabled);\n  }\n\n  @Override\n  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n    return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);\n  }\n\n  @Override\n  public int getPeriodCount() {\n    return timeline.getPeriodCount();\n  }\n\n  @Override\n  public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    return timeline.getPeriod(periodIndex, period, setIds);\n  }\n\n  @Override\n  public int getIndexOfPeriod(Object uid) {\n    return timeline.getIndexOfPeriod(uid);\n  }\n\n  @Override\n  public Object getUidOfPeriod(int periodIndex) {\n    return timeline.getUidOfPeriod(periodIndex);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Splits ICY stream metadata out from a stream.\n *\n * <p>Note: {@link #open(DataSpec)} and {@link #close()} are not supported. This implementation is\n * intended to wrap upstream {@link DataSource} instances that are opened and closed directly.\n */\n/* package */ final class IcyDataSource implements DataSource {\n\n  public interface Listener {\n\n    /**\n     * Called when ICY stream metadata has been split from the stream.\n     *\n     * @param metadata The stream metadata in binary form.\n     */\n    void onIcyMetadata(ParsableByteArray metadata);\n  }\n\n  private final DataSource upstream;\n  private final int metadataIntervalBytes;\n  private final Listener listener;\n  private final byte[] metadataLengthByteHolder;\n  private int bytesUntilMetadata;\n\n  /**\n   * @param upstream The upstream {@link DataSource}.\n   * @param metadataIntervalBytes The interval between ICY stream metadata, in bytes.\n   * @param listener A listener to which stream metadata is delivered.\n   */\n  public IcyDataSource(DataSource upstream, int metadataIntervalBytes, Listener listener) {\n    Assertions.checkArgument(metadataIntervalBytes > 0);\n    this.upstream = upstream;\n    this.metadataIntervalBytes = metadataIntervalBytes;\n    this.listener = listener;\n    metadataLengthByteHolder = new byte[1];\n    bytesUntilMetadata = metadataIntervalBytes;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    upstream.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    if (bytesUntilMetadata == 0) {\n      if (readMetadata()) {\n        bytesUntilMetadata = metadataIntervalBytes;\n      } else {\n        return C.RESULT_END_OF_INPUT;\n      }\n    }\n    int bytesRead = upstream.read(buffer, offset, Math.min(bytesUntilMetadata, readLength));\n    if (bytesRead != C.RESULT_END_OF_INPUT) {\n      bytesUntilMetadata -= bytesRead;\n    }\n    return bytesRead;\n  }\n\n  @Nullable\n  @Override\n  public Uri getUri() {\n    return upstream.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return upstream.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    throw new UnsupportedOperationException();\n  }\n\n  /**\n   * Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty.\n   *\n   * @return True if the block was extracted, including if its length byte indicated a length of\n   *     zero. False if the end of the stream was reached.\n   * @throws IOException If an error occurs reading from the wrapped {@link DataSource}.\n   */\n  private boolean readMetadata() throws IOException {\n    int bytesRead = upstream.read(metadataLengthByteHolder, 0, 1);\n    if (bytesRead == C.RESULT_END_OF_INPUT) {\n      return false;\n    }\n    int metadataLength = (metadataLengthByteHolder[0] & 0xFF) << 4;\n    if (metadataLength == 0) {\n      return true;\n    }\n\n    int offset = 0;\n    int lengthRemaining = metadataLength;\n    byte[] metadata = new byte[metadataLength];\n    while (lengthRemaining > 0) {\n      bytesRead = upstream.read(metadata, offset, lengthRemaining);\n      if (bytesRead == C.RESULT_END_OF_INPUT) {\n        return false;\n      }\n      offset += bytesRead;\n      lengthRemaining -= bytesRead;\n    }\n\n    // Discard trailing zero bytes.\n    while (metadataLength > 0 && metadata[metadataLength - 1] == 0) {\n      metadataLength--;\n    }\n\n    if (metadataLength > 0) {\n      listener.onIcyMetadata(new ParsableByteArray(metadata, metadataLength));\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Loops a {@link MediaSource} a specified number of times.\n *\n * <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link\n * ExoPlayer#setRepeatMode(int)} instead of this class.\n */\npublic final class LoopingMediaSource extends CompositeMediaSource<Void> {\n\n  private final MediaSource childSource;\n  private final int loopCount;\n  private final Map<MediaPeriodId, MediaPeriodId> childMediaPeriodIdToMediaPeriodId;\n  private final Map<MediaPeriod, MediaPeriodId> mediaPeriodToChildMediaPeriodId;\n\n  /**\n   * Loops the provided source indefinitely. Note that it is usually better to use\n   * {@link ExoPlayer#setRepeatMode(int)}.\n   *\n   * @param childSource The {@link MediaSource} to loop.\n   */\n  public LoopingMediaSource(MediaSource childSource) {\n    this(childSource, Integer.MAX_VALUE);\n  }\n\n  /**\n   * Loops the provided source a specified number of times.\n   *\n   * @param childSource The {@link MediaSource} to loop.\n   * @param loopCount The desired number of loops. Must be strictly positive.\n   */\n  public LoopingMediaSource(MediaSource childSource, int loopCount) {\n    Assertions.checkArgument(loopCount > 0);\n    this.childSource = childSource;\n    this.loopCount = loopCount;\n    childMediaPeriodIdToMediaPeriodId = new HashMap<>();\n    mediaPeriodToChildMediaPeriodId = new HashMap<>();\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return childSource.getTag();\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    prepareChildSource(/* id= */ null, childSource);\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    if (loopCount == Integer.MAX_VALUE) {\n      return childSource.createPeriod(id, allocator, startPositionUs);\n    }\n    Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);\n    MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);\n    childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);\n    MediaPeriod mediaPeriod =\n        childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);\n    mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    childSource.releasePeriod(mediaPeriod);\n    MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod);\n    if (childMediaPeriodId != null) {\n      childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId);\n    }\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {\n    Timeline loopingTimeline =\n        loopCount != Integer.MAX_VALUE\n            ? new LoopingTimeline(timeline, loopCount)\n            : new InfinitelyLoopingTimeline(timeline);\n    refreshSourceInfo(loopingTimeline);\n  }\n\n  @Override\n  protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      Void id, MediaPeriodId mediaPeriodId) {\n    return loopCount != Integer.MAX_VALUE\n        ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId)\n        : mediaPeriodId;\n  }\n\n  private static final class LoopingTimeline extends AbstractConcatenatedTimeline {\n\n    private final Timeline childTimeline;\n    private final int childPeriodCount;\n    private final int childWindowCount;\n    private final int loopCount;\n\n    public LoopingTimeline(Timeline childTimeline, int loopCount) {\n      super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount));\n      this.childTimeline = childTimeline;\n      childPeriodCount = childTimeline.getPeriodCount();\n      childWindowCount = childTimeline.getWindowCount();\n      this.loopCount = loopCount;\n      if (childPeriodCount > 0) {\n        Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount,\n            \"LoopingMediaSource contains too many periods\");\n      }\n    }\n\n    @Override\n    public int getWindowCount() {\n      return childWindowCount * loopCount;\n    }\n\n    @Override\n    public int getPeriodCount() {\n      return childPeriodCount * loopCount;\n    }\n\n    @Override\n    protected int getChildIndexByPeriodIndex(int periodIndex) {\n      return periodIndex / childPeriodCount;\n    }\n\n    @Override\n    protected int getChildIndexByWindowIndex(int windowIndex) {\n      return windowIndex / childWindowCount;\n    }\n\n    @Override\n    protected int getChildIndexByChildUid(Object childUid) {\n      if (!(childUid instanceof Integer)) {\n        return C.INDEX_UNSET;\n      }\n      return (Integer) childUid;\n    }\n\n    @Override\n    protected Timeline getTimelineByChildIndex(int childIndex) {\n      return childTimeline;\n    }\n\n    @Override\n    protected int getFirstPeriodIndexByChildIndex(int childIndex) {\n      return childIndex * childPeriodCount;\n    }\n\n    @Override\n    protected int getFirstWindowIndexByChildIndex(int childIndex) {\n      return childIndex * childWindowCount;\n    }\n\n    @Override\n    protected Object getChildUidByChildIndex(int childIndex) {\n      return childIndex;\n    }\n\n  }\n\n  private static final class InfinitelyLoopingTimeline extends ForwardingTimeline {\n\n    public InfinitelyLoopingTimeline(Timeline timeline) {\n      super(timeline);\n    }\n\n    @Override\n    public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n        boolean shuffleModeEnabled) {\n      int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode,\n          shuffleModeEnabled);\n      return childNextWindowIndex == C.INDEX_UNSET ? getFirstWindowIndex(shuffleModeEnabled)\n          : childNextWindowIndex;\n    }\n\n    @Override\n    public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,\n        boolean shuffleModeEnabled) {\n      int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode,\n          shuffleModeEnabled);\n      return childPreviousWindowIndex == C.INDEX_UNSET ? getLastWindowIndex(shuffleModeEnabled)\n          : childPreviousWindowIndex;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport java.io.IOException;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Media period that wraps a media source and defers calling its {@link\n * MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link\n * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media\n * period immediately but the media source that should create it is not yet prepared.\n */\npublic final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {\n\n  /** Listener for preparation errors. */\n  public interface PrepareErrorListener {\n\n    /**\n     * Called the first time an error occurs while refreshing source info or preparing the period.\n     */\n    void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception);\n  }\n\n  /** The {@link MediaSource} which will create the actual media period. */\n  public final MediaSource mediaSource;\n  /** The {@link MediaPeriodId} used to create the masking media period. */\n  public final MediaPeriodId id;\n\n  private final Allocator allocator;\n\n  @Nullable private MediaPeriod mediaPeriod;\n  @Nullable private Callback callback;\n  private long preparePositionUs;\n  @Nullable private PrepareErrorListener listener;\n  private boolean notifiedPrepareError;\n  private long preparePositionOverrideUs;\n\n  /**\n   * Creates a new masking media period.\n   *\n   * @param mediaSource The media source to wrap.\n   * @param id The identifier used to create the masking media period.\n   * @param allocator The allocator used to create the media period.\n   * @param preparePositionUs The expected start position, in microseconds.\n   */\n  public MaskingMediaPeriod(\n      MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {\n    this.id = id;\n    this.allocator = allocator;\n    this.mediaSource = mediaSource;\n    this.preparePositionUs = preparePositionUs;\n    preparePositionOverrideUs = C.TIME_UNSET;\n  }\n\n  /**\n   * Sets a listener for preparation errors.\n   *\n   * @param listener An listener to be notified of media period preparation errors. If a listener is\n   *     set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first\n   *     preparation error (if any) to the listener.\n   */\n  public void setPrepareErrorListener(PrepareErrorListener listener) {\n    this.listener = listener;\n  }\n\n  /** Returns the position at which the masking media period was prepared, in microseconds. */\n  public long getPreparePositionUs() {\n    return preparePositionUs;\n  }\n\n  /**\n   * Overrides the default prepare position at which to prepare the media period. This value is only\n   * used if called before {@link #createPeriod(MediaPeriodId)}.\n   *\n   * @param preparePositionUs The default prepare position to use, in microseconds.\n   */\n  public void overridePreparePositionUs(long preparePositionUs) {\n    preparePositionOverrideUs = preparePositionUs;\n  }\n\n  /**\n   * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source\n   * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link\n   * #releasePeriod()} to release the period.\n   *\n   * @param id The identifier that should be used to create the media period from the media source.\n   */\n  public void createPeriod(MediaPeriodId id) {\n    long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);\n    mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);\n    if (callback != null) {\n      mediaPeriod.prepare(this, preparePositionUs);\n    }\n  }\n\n  /**\n   * Releases the period.\n   */\n  public void releasePeriod() {\n    if (mediaPeriod != null) {\n      mediaSource.releasePeriod(mediaPeriod);\n    }\n  }\n\n  @Override\n  public void prepare(Callback callback, long preparePositionUs) {\n    this.callback = callback;\n    if (mediaPeriod != null) {\n      mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));\n    }\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    try {\n      if (mediaPeriod != null) {\n        mediaPeriod.maybeThrowPrepareError();\n      } else {\n        mediaSource.maybeThrowSourceInfoRefreshError();\n      }\n    } catch (final IOException e) {\n      if (listener == null) {\n        throw e;\n      }\n      if (!notifiedPrepareError) {\n        notifiedPrepareError = true;\n        listener.onPrepareError(id, e);\n      }\n    }\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return castNonNull(mediaPeriod).getTrackGroups();\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) {\n      positionUs = preparePositionOverrideUs;\n      preparePositionOverrideUs = C.TIME_UNSET;\n    }\n    return castNonNull(mediaPeriod)\n        .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs);\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe);\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    return castNonNull(mediaPeriod).readDiscontinuity();\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return castNonNull(mediaPeriod).getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    return castNonNull(mediaPeriod).seekToUs(positionUs);\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters);\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return castNonNull(mediaPeriod).getNextLoadPositionUs();\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    castNonNull(mediaPeriod).reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);\n  }\n\n  @Override\n  public boolean isLoading() {\n    return mediaPeriod != null && mediaPeriod.isLoading();\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod source) {\n    castNonNull(callback).onContinueLoadingRequested(this);\n  }\n\n  // MediaPeriod.Callback implementation\n\n  @Override\n  public void onPrepared(MediaPeriod mediaPeriod) {\n    castNonNull(callback).onPrepared(this);\n  }\n\n  private long getPreparePositionWithOverride(long preparePositionUs) {\n    return preparePositionOverrideUs != C.TIME_UNSET\n        ? preparePositionOverrideUs\n        : preparePositionUs;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Window;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media\n * structure is known.\n */\npublic final class MaskingMediaSource extends CompositeMediaSource<Void> {\n\n  private final MediaSource mediaSource;\n  private final boolean useLazyPreparation;\n  private final Timeline.Window window;\n  private final Timeline.Period period;\n\n  private MaskingTimeline timeline;\n  @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod;\n  @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher;\n  private boolean hasStartedPreparing;\n  private boolean isPrepared;\n\n  /**\n   * Creates the masking media source.\n   *\n   * @param mediaSource A {@link MediaSource}.\n   * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all\n   *     manifest loads and other initial preparation steps happen immediately. If true, these\n   *     initial preparations are triggered only when the player starts buffering the media.\n   */\n  public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {\n    this.mediaSource = mediaSource;\n    this.useLazyPreparation = useLazyPreparation;\n    window = new Timeline.Window();\n    period = new Timeline.Period();\n    timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());\n  }\n\n  /** Returns the {@link Timeline}. */\n  public Timeline getTimeline() {\n    return timeline;\n  }\n\n  @Override\n  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    if (!useLazyPreparation) {\n      hasStartedPreparing = true;\n      prepareChildSource(/* id= */ null, mediaSource);\n    }\n  }\n\n  @Nullable\n  @Override\n  public Object getTag() {\n    return mediaSource.getTag();\n  }\n\n  @Override\n  @SuppressWarnings(\"MissingSuperCall\")\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    // Do nothing. Source info refresh errors will be thrown when calling\n    // MaskingMediaPeriod.maybeThrowPrepareError.\n  }\n\n  @Override\n  public MaskingMediaPeriod createPeriod(\n      MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    MaskingMediaPeriod mediaPeriod =\n        new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);\n    if (isPrepared) {\n      MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid));\n      mediaPeriod.createPeriod(idInSource);\n    } else {\n      // We should have at most one media period while source is unprepared because the duration is\n      // unset and we don't load beyond periods with unset duration. We need to figure out how to\n      // handle the prepare positions of multiple deferred media periods, should that ever change.\n      unpreparedMaskingMediaPeriod = mediaPeriod;\n      unpreparedMaskingMediaPeriodEventDispatcher =\n          createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0);\n      unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated();\n      if (!hasStartedPreparing) {\n        hasStartedPreparing = true;\n        prepareChildSource(/* id= */ null, mediaSource);\n      }\n    }\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    ((MaskingMediaPeriod) mediaPeriod).releasePeriod();\n    if (mediaPeriod == unpreparedMaskingMediaPeriod) {\n      Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased();\n      unpreparedMaskingMediaPeriodEventDispatcher = null;\n      unpreparedMaskingMediaPeriod = null;\n    }\n  }\n\n  @Override\n  public void releaseSourceInternal() {\n    isPrepared = false;\n    hasStartedPreparing = false;\n    super.releaseSourceInternal();\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      Void id, MediaSource mediaSource, Timeline newTimeline) {\n    if (isPrepared) {\n      timeline = timeline.cloneWithUpdatedTimeline(newTimeline);\n    } else if (newTimeline.isEmpty()) {\n      timeline =\n          MaskingTimeline.createWithRealTimeline(\n              newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);\n    } else {\n      // Determine first period and the start position.\n      // This will be:\n      //  1. The default window start position if no deferred period has been created yet.\n      //  2. The non-zero prepare position of the deferred period under the assumption that this is\n      //     a non-zero initial seek position in the window.\n      //  3. The default window start position if the deferred period has a prepare position of zero\n      //     under the assumption that the prepare position of zero was used because it's the\n      //     default position of the DummyTimeline window. Note that this will override an\n      //     intentional seek to zero for a window with a non-zero default position. This is\n      //     unlikely to be a problem as a non-zero default position usually only occurs for live\n      //     playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions\n      //     anyway.\n      newTimeline.getWindow(/* windowIndex= */ 0, window);\n      long windowStartPositionUs = window.getDefaultPositionUs();\n      if (unpreparedMaskingMediaPeriod != null) {\n        long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();\n        if (periodPreparePositionUs != 0) {\n          windowStartPositionUs = periodPreparePositionUs;\n        }\n      }\n      Object windowUid = window.uid;\n      Pair<Object, Long> periodPosition =\n          newTimeline.getPeriodPosition(\n              window, period, /* windowIndex= */ 0, windowStartPositionUs);\n      Object periodUid = periodPosition.first;\n      long periodPositionUs = periodPosition.second;\n      timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);\n      if (unpreparedMaskingMediaPeriod != null) {\n        MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;\n        maskingPeriod.overridePreparePositionUs(periodPositionUs);\n        MediaPeriodId idInSource =\n            maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid));\n        maskingPeriod.createPeriod(idInSource);\n      }\n    }\n    isPrepared = true;\n    refreshSourceInfo(this.timeline);\n  }\n\n  @Nullable\n  @Override\n  protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      Void id, MediaPeriodId mediaPeriodId) {\n    return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid));\n  }\n\n  @Override\n  protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) {\n    // Suppress create and release events for the period created while the source was still\n    // unprepared, as we send these events from this class.\n    return unpreparedMaskingMediaPeriod == null\n        || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id);\n  }\n\n  private Object getInternalPeriodUid(Object externalPeriodUid) {\n    return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)\n        ? timeline.replacedInternalPeriodUid\n        : externalPeriodUid;\n  }\n\n  private Object getExternalPeriodUid(Object internalPeriodUid) {\n    return timeline.replacedInternalPeriodUid.equals(internalPeriodUid)\n        ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID\n        : internalPeriodUid;\n  }\n\n  /**\n   * Timeline used as placeholder for an unprepared media source. After preparation, a\n   * MaskingTimeline is used to keep the originally assigned dummy period ID.\n   */\n  private static final class MaskingTimeline extends ForwardingTimeline {\n\n    public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object();\n\n    private final Object replacedInternalWindowUid;\n    private final Object replacedInternalPeriodUid;\n\n    /**\n     * Returns an instance with a dummy timeline using the provided window tag.\n     *\n     * @param windowTag A window tag.\n     */\n    public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) {\n      return new MaskingTimeline(\n          new DummyTimeline(windowTag), Window.SINGLE_WINDOW_UID, DUMMY_EXTERNAL_PERIOD_UID);\n    }\n\n    /**\n     * Returns an instance with a real timeline, replacing the provided period ID with the already\n     * assigned dummy period ID.\n     *\n     * @param timeline The real timeline.\n     * @param firstWindowUid The window UID in the timeline which will be replaced by the already\n     *     assigned {@link Window#SINGLE_WINDOW_UID}.\n     * @param firstPeriodUid The period UID in the timeline which will be replaced by the already\n     *     assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}.\n     */\n    public static MaskingTimeline createWithRealTimeline(\n        Timeline timeline, Object firstWindowUid, Object firstPeriodUid) {\n      return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);\n    }\n\n    private MaskingTimeline(\n        Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) {\n      super(timeline);\n      this.replacedInternalWindowUid = replacedInternalWindowUid;\n      this.replacedInternalPeriodUid = replacedInternalPeriodUid;\n    }\n\n    /**\n     * Returns a copy with an updated timeline. This keeps the existing period replacement.\n     *\n     * @param timeline The new timeline.\n     */\n    public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) {\n      return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid);\n    }\n\n    /** Returns the wrapped timeline. */\n    public Timeline getTimeline() {\n      return timeline;\n    }\n\n    @Override\n    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n      timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);\n      if (Util.areEqual(window.uid, replacedInternalWindowUid)) {\n        window.uid = Window.SINGLE_WINDOW_UID;\n      }\n      return window;\n    }\n\n    @Override\n    public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n      timeline.getPeriod(periodIndex, period, setIds);\n      if (Util.areEqual(period.uid, replacedInternalPeriodUid)) {\n        period.uid = DUMMY_EXTERNAL_PERIOD_UID;\n      }\n      return period;\n    }\n\n    @Override\n    public int getIndexOfPeriod(Object uid) {\n      return timeline.getIndexOfPeriod(\n          DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid);\n    }\n\n    @Override\n    public Object getUidOfPeriod(int periodIndex) {\n      Object uid = timeline.getUidOfPeriod(periodIndex);\n      return Util.areEqual(uid, replacedInternalPeriodUid) ? DUMMY_EXTERNAL_PERIOD_UID : uid;\n    }\n  }\n\n  /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */\n  private static final class DummyTimeline extends Timeline {\n\n    @Nullable private final Object tag;\n\n    public DummyTimeline(@Nullable Object tag) {\n      this.tag = tag;\n    }\n\n    @Override\n    public int getWindowCount() {\n      return 1;\n    }\n\n    @Override\n    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n      return window.set(\n          Window.SINGLE_WINDOW_UID,\n          tag,\n          /* manifest= */ null,\n          /* presentationStartTimeMs= */ C.TIME_UNSET,\n          /* windowStartTimeMs= */ C.TIME_UNSET,\n          /* isSeekable= */ false,\n          // Dynamic window to indicate pending timeline updates.\n          /* isDynamic= */ true,\n          /* isLive= */ false,\n          /* defaultPositionUs= */ 0,\n          /* durationUs= */ C.TIME_UNSET,\n          /* firstPeriodIndex= */ 0,\n          /* lastPeriodIndex= */ 0,\n          /* positionInFirstPeriodUs= */ 0);\n    }\n\n    @Override\n    public int getPeriodCount() {\n      return 1;\n    }\n\n    @Override\n    public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n      return period.set(\n          /* id= */ 0,\n          /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID,\n          /* windowIndex= */ 0,\n          /* durationUs = */ C.TIME_UNSET,\n          /* positionInWindowUs= */ 0);\n    }\n\n    @Override\n    public int getIndexOfPeriod(Object uid) {\n      return uid == MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID ? 0 : C.INDEX_UNSET;\n    }\n\n    @Override\n    public Object getUidOfPeriod(int periodIndex) {\n      return MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All\n * methods are called on the player's internal playback thread, as described in the\n * {@link ExoPlayer} Javadoc.\n */\npublic interface MediaPeriod extends SequenceableLoader {\n\n  /**\n   * A callback to be notified of {@link MediaPeriod} events.\n   */\n  interface Callback extends SequenceableLoader.Callback<MediaPeriod> {\n\n    /**\n     * Called when preparation completes.\n     *\n     * <p>Called on the playback thread. After invoking this method, the {@link MediaPeriod} can\n     * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],\n     * long)} to be called with the initial track selection.\n     *\n     * @param mediaPeriod The prepared {@link MediaPeriod}.\n     */\n    void onPrepared(MediaPeriod mediaPeriod);\n  }\n\n  /**\n   * Prepares this media period asynchronously.\n   *\n   * <p>{@code callback.onPrepared} is called when preparation completes. If preparation fails,\n   * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.\n   *\n   * <p>If preparation succeeds and results in a source timeline change (e.g. the period duration\n   * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be\n   * called before {@code callback.onPrepared}.\n   *\n   * @param callback Callback to receive updates from this period, including being notified when\n   *     preparation completes.\n   * @param positionUs The expected starting position, in microseconds.\n   */\n  void prepare(Callback callback, long positionUs);\n\n  /**\n   * Throws an error that's preventing the period from becoming prepared. Does nothing if no such\n   * error exists.\n   *\n   * <p>This method is only called before the period has completed preparation.\n   *\n   * @throws IOException The underlying error.\n   */\n  void maybeThrowPrepareError() throws IOException;\n\n  /**\n   * Returns the {@link TrackGroup}s exposed by the period.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * @return The {@link TrackGroup}s.\n   */\n  TrackGroupArray getTrackGroups();\n\n  /**\n   * Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period\n   * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for\n   *     which stream keys are requested.\n   * @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty\n   *     list if filtering is not possible and the entire media needs to be loaded to play the\n   *     selected tracks.\n   */\n  default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {\n    return Collections.emptyList();\n  }\n\n  /**\n   * Performs a track selection.\n   *\n   * <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}\n   * indicating whether the existing {@link SampleStream} can be retained for each selection, and\n   * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the\n   * provided selections, clearing, setting and replacing entries as required. If an existing sample\n   * stream is retained but with the requirement that the consuming renderer be reset, then the\n   * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set\n   * if a new sample stream is created.\n   *\n   * <p>Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and\n   * any references to them must be updated to point to the new selections.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * @param selections The renderer track selections.\n   * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained\n   *     for each track selection. A {@code true} value indicates that the selection is equivalent\n   *     to the one that was previously passed, and that the caller does not require that the sample\n   *     stream be recreated. If a retained sample stream holds any references to the track\n   *     selection then they must be updated to point to the new selection.\n   * @param streams The existing sample streams, which will be updated to reflect the provided\n   *     selections.\n   * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that\n   *     have been retained but with the requirement that the consuming renderer be reset.\n   * @param positionUs The current playback position in microseconds. If playback of this period has\n   *     not yet started, the value will be the starting position.\n   * @return The actual position at which the tracks were enabled, in microseconds.\n   */\n  long selectTracks(\n          @NullableType TrackSelection[] selections,\n          boolean[] mayRetainStreamFlags,\n          @NullableType SampleStream[] streams,\n          boolean[] streamResetFlags,\n          long positionUs);\n\n  /**\n   * Discards buffered media up to the specified position.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * @param positionUs The position in microseconds.\n   * @param toKeyframe If true then for each track discards samples up to the keyframe before or at\n   *     the specified position, rather than any sample before or at that position.\n   */\n  void discardBuffer(long positionUs, boolean toKeyframe);\n\n  /**\n   * Attempts to read a discontinuity.\n   *\n   * <p>After this method has returned a value other than {@link C#TIME_UNSET}, all {@link\n   * SampleStream}s provided by the period are guaranteed to start from a key frame.\n   *\n   * <p>This method is only called after the period has been prepared and before reading from any\n   * {@link SampleStream}s provided by the period.\n   *\n   * @return If a discontinuity was read then the playback position in microseconds after the\n   *     discontinuity. Else {@link C#TIME_UNSET}.\n   */\n  long readDiscontinuity();\n\n  /**\n   * Attempts to seek to the specified position in microseconds.\n   *\n   * <p>After this method has been called, all {@link SampleStream}s provided by the period are\n   * guaranteed to start from a key frame.\n   *\n   * <p>This method is only called when at least one track is selected.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @return The actual position to which the period was seeked, in microseconds.\n   */\n  long seekToUs(long positionUs);\n\n  /**\n   * Returns the position to which a seek will be performed, given the specified seek position and\n   * {@link SeekParameters}.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @param seekParameters Parameters that control how the seek is performed. Implementations may\n   *     apply seek parameters on a best effort basis.\n   * @return The actual position to which a seek will be performed, in microseconds.\n   */\n  long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);\n\n  // SequenceableLoader interface. Overridden to provide more specific documentation.\n\n  /**\n   * Returns an estimate of the position up to which data is buffered for the enabled tracks.\n   *\n   * <p>This method is only called when at least one track is selected.\n   *\n   * @return An estimate of the absolute position in microseconds up to which data is buffered, or\n   *     {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.\n   */\n  @Override\n  long getBufferedPositionUs();\n\n  /**\n   * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.\n   *\n   * <p>This method is only called after the period has been prepared. It may be called when no\n   * tracks are selected.\n   */\n  @Override\n  long getNextLoadPositionUs();\n\n  /**\n   * Attempts to continue loading.\n   *\n   * <p>This method may be called both during and after the period has been prepared.\n   *\n   * <p>A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the\n   * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be\n   * called when the period is permitted to continue loading data. A period may do this both during\n   * and after preparation.\n   *\n   * @param positionUs The current playback position in microseconds. If playback of this period has\n   *     not yet started, the value will be the starting position in this period minus the duration\n   *     of any media in previous periods still to be played.\n   * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a\n   *     different value than prior to the call. False otherwise.\n   */\n  @Override\n  boolean continueLoading(long positionUs);\n\n  /** Returns whether the media period is currently loading. */\n  boolean isLoading();\n\n  /**\n   * Re-evaluates the buffer given the playback position.\n   *\n   * <p>This method is only called after the period has been prepared.\n   *\n   * <p>A period may choose to discard buffered media so that it can be re-buffered in a different\n   * quality.\n   *\n   * @param positionUs The current playback position in microseconds. If playback of this period has\n   *     not yet started, the value will be the starting position in this period minus the duration\n   *     of any media in previous periods still to be played.\n   */\n  @Override\n  void reevaluateBuffer(long positionUs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.io.IOException;\n\n/**\n * Defines and provides media to be played by an {@link com.google.android.exoplayer2.ExoPlayer}. A\n * MediaSource has two main responsibilities:\n *\n * <ul>\n *   <li>To provide the player with a {@link Timeline} defining the structure of its media, and to\n *       provide a new timeline whenever the structure of the media changes. The MediaSource\n *       provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the\n *       {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller,\n *       TransferListener)}.\n *   <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are\n *       obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a\n *       way for the player to load and read the media.\n * </ul>\n *\n * All methods are called on the player's internal playback thread, as described in the {@link\n * com.google.android.exoplayer2.ExoPlayer} Javadoc. They should not be called directly from\n * application code. Instances can be re-used, but only for one {@link\n * com.google.android.exoplayer2.ExoPlayer} instance simultaneously.\n */\npublic interface MediaSource {\n\n  /** A caller of media sources, which will be notified of source events. */\n  interface MediaSourceCaller {\n\n    /**\n     * Called when the {@link Timeline} has been refreshed.\n     *\n     * <p>Called on the playback thread.\n     *\n     * @param source The {@link MediaSource} whose info has been refreshed.\n     * @param timeline The source's timeline.\n     */\n    void onSourceInfoRefreshed(MediaSource source, Timeline timeline);\n  }\n\n  /** Identifier for a {@link MediaPeriod}. */\n  final class MediaPeriodId {\n\n    /** The unique id of the timeline period. */\n    public final Object periodUid;\n\n    /**\n     * If the media period is in an ad group, the index of the ad group in the period.\n     * {@link C#INDEX_UNSET} otherwise.\n     */\n    public final int adGroupIndex;\n\n    /**\n     * If the media period is in an ad group, the index of the ad in its ad group in the period.\n     * {@link C#INDEX_UNSET} otherwise.\n     */\n    public final int adIndexInAdGroup;\n\n    /**\n     * The sequence number of the window in the buffered sequence of windows this media period is\n     * part of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of\n     * windows.\n     */\n    public final long windowSequenceNumber;\n\n    /**\n     * The index of the next ad group to which the media period's content is clipped, or {@link\n     * C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.\n     */\n    public final int nextAdGroupIndex;\n\n    /**\n     * Creates a media period identifier for a dummy period which is not part of a buffered sequence\n     * of windows.\n     *\n     * @param periodUid The unique id of the timeline period.\n     */\n    public MediaPeriodId(Object periodUid) {\n      this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET);\n    }\n\n    /**\n     * Creates a media period identifier for the specified period in the timeline.\n     *\n     * @param periodUid The unique id of the timeline period.\n     * @param windowSequenceNumber The sequence number of the window in the buffered sequence of\n     *     windows this media period is part of.\n     */\n    public MediaPeriodId(Object periodUid, long windowSequenceNumber) {\n      this(\n          periodUid,\n          /* adGroupIndex= */ C.INDEX_UNSET,\n          /* adIndexInAdGroup= */ C.INDEX_UNSET,\n          windowSequenceNumber,\n          /* nextAdGroupIndex= */ C.INDEX_UNSET);\n    }\n\n    /**\n     * Creates a media period identifier for the specified clipped period in the timeline.\n     *\n     * @param periodUid The unique id of the timeline period.\n     * @param windowSequenceNumber The sequence number of the window in the buffered sequence of\n     *     windows this media period is part of.\n     * @param nextAdGroupIndex The index of the next ad group to which the media period's content is\n     *     clipped.\n     */\n    public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {\n      this(\n          periodUid,\n          /* adGroupIndex= */ C.INDEX_UNSET,\n          /* adIndexInAdGroup= */ C.INDEX_UNSET,\n          windowSequenceNumber,\n          nextAdGroupIndex);\n    }\n\n    /**\n     * Creates a media period identifier that identifies an ad within an ad group at the specified\n     * timeline period.\n     *\n     * @param periodUid The unique id of the timeline period that contains the ad group.\n     * @param adGroupIndex The index of the ad group.\n     * @param adIndexInAdGroup The index of the ad in the ad group.\n     * @param windowSequenceNumber The sequence number of the window in the buffered sequence of\n     *     windows this media period is part of.\n     */\n    public MediaPeriodId(\n        Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {\n      this(\n          periodUid,\n          adGroupIndex,\n          adIndexInAdGroup,\n          windowSequenceNumber,\n          /* nextAdGroupIndex= */ C.INDEX_UNSET);\n    }\n\n    private MediaPeriodId(\n        Object periodUid,\n        int adGroupIndex,\n        int adIndexInAdGroup,\n        long windowSequenceNumber,\n        int nextAdGroupIndex) {\n      this.periodUid = periodUid;\n      this.adGroupIndex = adGroupIndex;\n      this.adIndexInAdGroup = adIndexInAdGroup;\n      this.windowSequenceNumber = windowSequenceNumber;\n      this.nextAdGroupIndex = nextAdGroupIndex;\n    }\n\n    /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */\n    public MediaPeriodId copyWithPeriodUid(Object newPeriodUid) {\n      return periodUid.equals(newPeriodUid)\n          ? this\n          : new MediaPeriodId(\n              newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);\n    }\n\n    /**\n     * Returns whether this period identifier identifies an ad in an ad group in a period.\n     */\n    public boolean isAd() {\n      return adGroupIndex != C.INDEX_UNSET;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n\n      MediaPeriodId periodId = (MediaPeriodId) obj;\n      return periodUid.equals(periodId.periodUid)\n          && adGroupIndex == periodId.adGroupIndex\n          && adIndexInAdGroup == periodId.adIndexInAdGroup\n          && windowSequenceNumber == periodId.windowSequenceNumber\n          && nextAdGroupIndex == periodId.nextAdGroupIndex;\n    }\n\n    @Override\n    public int hashCode() {\n      int result = 17;\n      result = 31 * result + periodUid.hashCode();\n      result = 31 * result + adGroupIndex;\n      result = 31 * result + adIndexInAdGroup;\n      result = 31 * result + (int) windowSequenceNumber;\n      result = 31 * result + nextAdGroupIndex;\n      return result;\n    }\n  }\n\n  /**\n   * Adds a {@link MediaSourceEventListener} to the list of listeners which are notified of media\n   * source events.\n   *\n   * @param handler A handler on the which listener events will be posted.\n   * @param eventListener The listener to be added.\n   */\n  void addEventListener(Handler handler, MediaSourceEventListener eventListener);\n\n  /**\n   * Removes a {@link MediaSourceEventListener} from the list of listeners which are notified of\n   * media source events.\n   *\n   * @param eventListener The listener to be removed.\n   */\n  void removeEventListener(MediaSourceEventListener eventListener);\n\n  /** Returns the tag set on the media source, or null if none was set. */\n  @Nullable\n  default Object getTag() {\n    return null;\n  }\n\n  /**\n   * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the\n   * source for the creation of {@link MediaPeriod MediaPerods}.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>{@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once\n   * the source has a {@link Timeline}.\n   *\n   * <p>For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed\n   * to remove the caller and to release the source if no longer required.\n   *\n   * @param caller The {@link MediaSourceCaller} to be registered.\n   * @param mediaTransferListener The transfer listener which should be informed of any media data\n   *     transfers. May be null if no listener is available. Note that this listener should be only\n   *     informed of transfers related to the media loads and not of auxiliary loads for manifests\n   *     and other data.\n   */\n  void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener);\n\n  /**\n   * Throws any pending error encountered while loading or refreshing source information.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}.\n   */\n  void maybeThrowSourceInfoRefreshError() throws IOException;\n\n  /**\n   * Enables the source for the creation of {@link MediaPeriod MediaPeriods}.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}.\n   *\n   * @param caller The {@link MediaSourceCaller} enabling the source.\n   */\n  void enable(MediaSourceCaller caller);\n\n  /**\n   * Returns a new {@link MediaPeriod} identified by {@code periodId}.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>Must only be called if the source is enabled.\n   *\n   * @param id The identifier of the period.\n   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.\n   * @param startPositionUs The expected start position, in microseconds.\n   * @return A new {@link MediaPeriod}.\n   */\n  MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);\n\n  /**\n   * Releases the period.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * @param mediaPeriod The period to release.\n   */\n  void releasePeriod(MediaPeriod mediaPeriod);\n\n  /**\n   * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation\n   * should not hold onto limited resources used for the creation of media periods.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link\n   * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link\n   * #releasePeriod(MediaPeriod)}.\n   *\n   * @param caller The {@link MediaSourceCaller} disabling the source.\n   */\n  void disable(MediaSourceCaller caller);\n\n  /**\n   * Unregisters a caller, and disables and releases the source if no longer required.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by\n   * {@link #releasePeriod(MediaPeriod)}.\n   *\n   * @param caller The {@link MediaSourceCaller} to be unregistered.\n   */\n  void releaseSource(MediaSourceCaller caller);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/** Interface for callbacks to be notified of {@link MediaSource} events. */\npublic interface MediaSourceEventListener {\n\n  /** Media source load event information. */\n  final class LoadEventInfo {\n\n    /** Defines the requested data. */\n    public final DataSpec dataSpec;\n    /**\n     * The {@link Uri} from which data is being read. The uri will be identical to the one in {@link\n     * #dataSpec}.uri unless redirection has occurred. If redirection has occurred, this is the uri\n     * after redirection.\n     */\n    public final Uri uri;\n    /** The response headers associated with the load, or an empty map if unavailable. */\n    public final Map<String, List<String>> responseHeaders;\n    /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */\n    public final long elapsedRealtimeMs;\n    /** The duration of the load up to the event time. */\n    public final long loadDurationMs;\n    /** The number of bytes that were loaded up to the event time. */\n    public final long bytesLoaded;\n\n    /**\n     * Creates load event info.\n     *\n     * @param dataSpec Defines the requested data.\n     * @param uri The {@link Uri} from which data is being read. The uri must be identical to the\n     *     one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred,\n     *     this is the uri after redirection.\n     * @param responseHeaders The response headers associated with the load, or an empty map if\n     *     unavailable.\n     * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the\n     *     load event.\n     * @param loadDurationMs The duration of the load up to the event time.\n     * @param bytesLoaded The number of bytes that were loaded up to the event time. For compressed\n     *     network responses, this is the decompressed size.\n     */\n    public LoadEventInfo(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded) {\n      this.dataSpec = dataSpec;\n      this.uri = uri;\n      this.responseHeaders = responseHeaders;\n      this.elapsedRealtimeMs = elapsedRealtimeMs;\n      this.loadDurationMs = loadDurationMs;\n      this.bytesLoaded = bytesLoaded;\n    }\n  }\n\n  /** Descriptor for data being loaded or selected by a media source. */\n  final class MediaLoadData {\n\n    /** One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. */\n    public final int dataType;\n    /**\n     * One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to media of a\n     * specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.\n     */\n    public final int trackType;\n    /**\n     * The format of the track to which the data belongs. Null if the data does not belong to a\n     * specific track.\n     */\n    @Nullable public final Format trackFormat;\n    /**\n     * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track.\n     * {@link C#SELECTION_REASON_UNKNOWN} otherwise.\n     */\n    public final int trackSelectionReason;\n    /**\n     * Optional data associated with the selection of the track to which the data belongs. Null if\n     * the data does not belong to a track.\n     */\n    @Nullable public final Object trackSelectionData;\n    /**\n     * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a\n     * specific media period.\n     */\n    public final long mediaStartTimeMs;\n    /**\n     * The end time of the media, or {@link C#TIME_UNSET} if the data does not belong to a specific\n     * media period or the end time is unknown.\n     */\n    public final long mediaEndTimeMs;\n\n    /**\n     * Creates media load data.\n     *\n     * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data.\n     * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds\n     *     to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.\n     * @param trackFormat The format of the track to which the data belongs. Null if the data does\n     *     not belong to a track.\n     * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the\n     *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.\n     * @param trackSelectionData Optional data associated with the selection of the track to which\n     *     the data belongs. Null if the data does not belong to a track.\n     * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does\n     *     not belong to a specific media period.\n     * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not\n     *     belong to a specific media period or the end time is unknown.\n     */\n    public MediaLoadData(\n        int dataType,\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaStartTimeMs,\n        long mediaEndTimeMs) {\n      this.dataType = dataType;\n      this.trackType = trackType;\n      this.trackFormat = trackFormat;\n      this.trackSelectionReason = trackSelectionReason;\n      this.trackSelectionData = trackSelectionData;\n      this.mediaStartTimeMs = mediaStartTimeMs;\n      this.mediaEndTimeMs = mediaEndTimeMs;\n    }\n  }\n\n  /**\n   * Called when a media period is created by the media source.\n   *\n   * @param windowIndex The window index in the timeline this media period belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} of the created media period.\n   */\n  default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {}\n\n  /**\n   * Called when a media period is released by the media source.\n   *\n   * @param windowIndex The window index in the timeline this media period belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} of the released media period.\n   */\n  default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {}\n\n  /**\n   * Called when a load begins.\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not\n   *     belong to a specific media period.\n   * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The value of {@link\n   *     LoadEventInfo#uri} won't reflect potential redirection yet and {@link\n   *     LoadEventInfo#responseHeaders} will be empty.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadStarted(\n          int windowIndex,\n          @Nullable MediaPeriodId mediaPeriodId,\n          LoadEventInfo loadEventInfo,\n          MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a load ends.\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not\n   *     belong to a specific media period.\n   * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link\n   *     LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the\n   *     corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}\n   *     event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadCompleted(\n          int windowIndex,\n          @Nullable MediaPeriodId mediaPeriodId,\n          LoadEventInfo loadEventInfo,\n          MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a load is canceled.\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not\n   *     belong to a specific media period.\n   * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link\n   *     LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the\n   *     corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}\n   *     event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   */\n  default void onLoadCanceled(\n          int windowIndex,\n          @Nullable MediaPeriodId mediaPeriodId,\n          LoadEventInfo loadEventInfo,\n          MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a load error occurs.\n   *\n   * <p>The error may or may not have resulted in the load being canceled, as indicated by the\n   * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will\n   * <em>not</em> be called in addition to this method.\n   *\n   * <p>This method being called does not indicate that playback has failed, or that it will fail.\n   * The player may be able to recover from the error and continue. Hence applications should\n   * <em>not</em> implement this method to display a user visible error or initiate an application\n   * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement\n   * such behavior). This method is called to provide the application with an opportunity to log the\n   * error if it wishes to do so.\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not\n   *     belong to a specific media period.\n   * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link\n   *     LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the\n   *     corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}\n   *     event.\n   * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.\n   * @param error The load error.\n   * @param wasCanceled Whether the load was canceled as a result of the error.\n   */\n  default void onLoadError(\n          int windowIndex,\n          @Nullable MediaPeriodId mediaPeriodId,\n          LoadEventInfo loadEventInfo,\n          MediaLoadData mediaLoadData,\n          IOException error,\n          boolean wasCanceled) {}\n\n  /**\n   * Called when a media period is first being read from.\n   *\n   * @param windowIndex The window index in the timeline this media period belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from.\n   */\n  default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {}\n\n  /**\n   * Called when data is removed from the back of a media buffer, typically so that it can be\n   * re-buffered in a different format.\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.\n   * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.\n   */\n  default void onUpstreamDiscarded(\n          int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}\n\n  /**\n   * Called when a downstream format change occurs (i.e. when the format of the media being read\n   * from one or more {@link SampleStream}s provided by the source changes).\n   *\n   * @param windowIndex The window index in the timeline of the media source this load belongs to.\n   * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.\n   * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data.\n   */\n  default void onDownstreamFormatChanged(\n          int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}\n\n  /** Dispatches events to {@link MediaSourceEventListener}s. */\n  final class EventDispatcher {\n\n    /** The timeline window index reported with the events. */\n    public final int windowIndex;\n    /** The {@link MediaPeriodId} reported with the events. */\n    @Nullable public final MediaPeriodId mediaPeriodId;\n\n    private final CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers;\n    private final long mediaTimeOffsetMs;\n\n    /** Creates an event dispatcher. */\n    public EventDispatcher() {\n      this(\n          /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(),\n          /* windowIndex= */ 0,\n          /* mediaPeriodId= */ null,\n          /* mediaTimeOffsetMs= */ 0);\n    }\n\n    private EventDispatcher(\n        CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers,\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        long mediaTimeOffsetMs) {\n      this.listenerAndHandlers = listenerAndHandlers;\n      this.windowIndex = windowIndex;\n      this.mediaPeriodId = mediaPeriodId;\n      this.mediaTimeOffsetMs = mediaTimeOffsetMs;\n    }\n\n    /**\n     * Creates a view of the event dispatcher with pre-configured window index, media period id, and\n     * media time offset.\n     *\n     * @param windowIndex The timeline window index to be reported with the events.\n     * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.\n     * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.\n     * @return A view of the event dispatcher with the pre-configured parameters.\n     */\n    @CheckResult\n    public EventDispatcher withParameters(\n        int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {\n      return new EventDispatcher(\n          listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs);\n    }\n\n    /**\n     * Adds a listener to the event dispatcher.\n     *\n     * @param handler A handler on the which listener events will be posted.\n     * @param eventListener The listener to be added.\n     */\n    public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {\n      Assertions.checkArgument(handler != null && eventListener != null);\n      listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener));\n    }\n\n    /**\n     * Removes a listener from the event dispatcher.\n     *\n     * @param eventListener The listener to be removed.\n     */\n    public void removeEventListener(MediaSourceEventListener eventListener) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        if (listenerAndHandler.listener == eventListener) {\n          listenerAndHandlers.remove(listenerAndHandler);\n        }\n      }\n    }\n\n    /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */\n    public void mediaPeriodCreated() {\n      MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId));\n      }\n    }\n\n    /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */\n    public void mediaPeriodReleased() {\n      MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId));\n      }\n    }\n\n    /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {\n      loadStarted(\n          dataSpec,\n          dataType,\n          C.TRACK_TYPE_UNKNOWN,\n          null,\n          C.SELECTION_REASON_UNKNOWN,\n          null,\n          C.TIME_UNSET,\n          C.TIME_UNSET,\n          elapsedRealtimeMs);\n    }\n\n    /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadStarted(\n        DataSpec dataSpec,\n        int dataType,\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaStartTimeUs,\n        long mediaEndTimeUs,\n        long elapsedRealtimeMs) {\n      loadStarted(\n          new LoadEventInfo(\n              dataSpec,\n              dataSpec.uri,\n              /* responseHeaders= */ Collections.emptyMap(),\n              elapsedRealtimeMs,\n              /* loadDurationMs= */ 0,\n              /* bytesLoaded= */ 0),\n          new MediaLoadData(\n              dataType,\n              trackType,\n              trackFormat,\n              trackSelectionReason,\n              trackSelectionData,\n              adjustMediaTime(mediaStartTimeUs),\n              adjustMediaTime(mediaEndTimeUs)));\n    }\n\n    /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));\n      }\n    }\n\n    /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCompleted(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded) {\n      loadCompleted(\n          dataSpec,\n          uri,\n          responseHeaders,\n          dataType,\n          C.TRACK_TYPE_UNKNOWN,\n          null,\n          C.SELECTION_REASON_UNKNOWN,\n          null,\n          C.TIME_UNSET,\n          C.TIME_UNSET,\n          elapsedRealtimeMs,\n          loadDurationMs,\n          bytesLoaded);\n    }\n\n    /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCompleted(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaStartTimeUs,\n        long mediaEndTimeUs,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded) {\n      loadCompleted(\n          new LoadEventInfo(\n              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),\n          new MediaLoadData(\n              dataType,\n              trackType,\n              trackFormat,\n              trackSelectionReason,\n              trackSelectionData,\n              adjustMediaTime(mediaStartTimeUs),\n              adjustMediaTime(mediaEndTimeUs)));\n    }\n\n    /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () ->\n                listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));\n      }\n    }\n\n    /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCanceled(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded) {\n      loadCanceled(\n          dataSpec,\n          uri,\n          responseHeaders,\n          dataType,\n          C.TRACK_TYPE_UNKNOWN,\n          null,\n          C.SELECTION_REASON_UNKNOWN,\n          null,\n          C.TIME_UNSET,\n          C.TIME_UNSET,\n          elapsedRealtimeMs,\n          loadDurationMs,\n          bytesLoaded);\n    }\n\n    /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCanceled(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaStartTimeUs,\n        long mediaEndTimeUs,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded) {\n      loadCanceled(\n          new LoadEventInfo(\n              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),\n          new MediaLoadData(\n              dataType,\n              trackType,\n              trackFormat,\n              trackSelectionReason,\n              trackSelectionData,\n              adjustMediaTime(mediaStartTimeUs),\n              adjustMediaTime(mediaEndTimeUs)));\n    }\n\n    /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */\n    public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () ->\n                listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));\n      }\n    }\n\n    /**\n     * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,\n     * boolean)}.\n     */\n    public void loadError(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded,\n        IOException error,\n        boolean wasCanceled) {\n      loadError(\n          dataSpec,\n          uri,\n          responseHeaders,\n          dataType,\n          C.TRACK_TYPE_UNKNOWN,\n          null,\n          C.SELECTION_REASON_UNKNOWN,\n          null,\n          C.TIME_UNSET,\n          C.TIME_UNSET,\n          elapsedRealtimeMs,\n          loadDurationMs,\n          bytesLoaded,\n          error,\n          wasCanceled);\n    }\n\n    /**\n     * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,\n     * boolean)}.\n     */\n    public void loadError(\n        DataSpec dataSpec,\n        Uri uri,\n        Map<String, List<String>> responseHeaders,\n        int dataType,\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaStartTimeUs,\n        long mediaEndTimeUs,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        long bytesLoaded,\n        IOException error,\n        boolean wasCanceled) {\n      loadError(\n          new LoadEventInfo(\n              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),\n          new MediaLoadData(\n              dataType,\n              trackType,\n              trackFormat,\n              trackSelectionReason,\n              trackSelectionData,\n              adjustMediaTime(mediaStartTimeUs),\n              adjustMediaTime(mediaEndTimeUs)),\n          error,\n          wasCanceled);\n    }\n\n    /**\n     * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,\n     * boolean)}.\n     */\n    public void loadError(\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData,\n        IOException error,\n        boolean wasCanceled) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () ->\n                listener.onLoadError(\n                    windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled));\n      }\n    }\n\n    /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */\n    public void readingStarted() {\n      MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onReadingStarted(windowIndex, mediaPeriodId));\n      }\n    }\n\n    /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */\n    public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) {\n      upstreamDiscarded(\n          new MediaLoadData(\n              C.DATA_TYPE_MEDIA,\n              trackType,\n              /* trackFormat= */ null,\n              C.SELECTION_REASON_ADAPTIVE,\n              /* trackSelectionData= */ null,\n              adjustMediaTime(mediaStartTimeUs),\n              adjustMediaTime(mediaEndTimeUs)));\n    }\n\n    /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */\n    public void upstreamDiscarded(MediaLoadData mediaLoadData) {\n      MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData));\n      }\n    }\n\n    /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */\n    public void downstreamFormatChanged(\n        int trackType,\n        @Nullable Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        long mediaTimeUs) {\n      downstreamFormatChanged(\n          new MediaLoadData(\n              C.DATA_TYPE_MEDIA,\n              trackType,\n              trackFormat,\n              trackSelectionReason,\n              trackSelectionData,\n              adjustMediaTime(mediaTimeUs),\n              /* mediaEndTimeMs= */ C.TIME_UNSET));\n    }\n\n    /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */\n    public void downstreamFormatChanged(MediaLoadData mediaLoadData) {\n      for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {\n        final MediaSourceEventListener listener = listenerAndHandler.listener;\n        postOrRun(\n            listenerAndHandler.handler,\n            () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData));\n      }\n    }\n\n    private long adjustMediaTime(long mediaTimeUs) {\n      long mediaTimeMs = C.usToMs(mediaTimeUs);\n      return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;\n    }\n\n    private void postOrRun(Handler handler, Runnable runnable) {\n      if (handler.getLooper() == Looper.myLooper()) {\n        runnable.run();\n      } else {\n        handler.post(runnable);\n      }\n    }\n\n    private static final class ListenerAndHandler {\n\n      public final Handler handler;\n      public final MediaSourceEventListener listener;\n\n      public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) {\n        this.handler = handler;\n        this.listener = listener;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport java.util.List;\n\n/** Factory for creating {@link MediaSource}s from URIs. */\npublic interface MediaSourceFactory {\n\n  /**\n   * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered.\n   *\n   * @param streamKeys A list of {@link StreamKey StreamKeys}.\n   * @return This factory, for convenience.\n   * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n   */\n  default MediaSourceFactory setStreamKeys(List<StreamKey> streamKeys) {\n    return this;\n  }\n\n  /**\n   * Creates a new {@link MediaSource} with the specified {@code uri}.\n   *\n   * @param uri The URI to play.\n   * @return The new {@link MediaSource media source}.\n   */\n  MediaSource createMediaSource(Uri uri);\n\n  /**\n   * Returns the {@link C.ContentType content types} supported by media sources created by this\n   * factory.\n   */\n  @C.ContentType\n  int[] getSupportedTypes();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.IdentityHashMap;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Merges multiple {@link MediaPeriod}s.\n */\n/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {\n\n  public final MediaPeriod[] periods;\n\n  private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final ArrayList<MediaPeriod> childrenPendingPreparation;\n\n  @Nullable private Callback callback;\n  @Nullable private TrackGroupArray trackGroups;\n  private MediaPeriod[] enabledPeriods;\n  private SequenceableLoader compositeSequenceableLoader;\n\n  public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      MediaPeriod... periods) {\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    this.periods = periods;\n    childrenPendingPreparation = new ArrayList<>();\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();\n    streamPeriodIndices = new IdentityHashMap<>();\n    enabledPeriods = new MediaPeriod[0];\n  }\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    Collections.addAll(childrenPendingPreparation, periods);\n    for (MediaPeriod period : periods) {\n      period.prepare(this, positionUs);\n    }\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    for (MediaPeriod period : periods) {\n      period.maybeThrowPrepareError();\n    }\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return Assertions.checkNotNull(trackGroups);\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    // Map each selection and stream onto a child period index.\n    int[] streamChildIndices = new int[selections.length];\n    int[] selectionChildIndices = new int[selections.length];\n    for (int i = 0; i < selections.length; i++) {\n      streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET\n          : streamPeriodIndices.get(streams[i]);\n      selectionChildIndices[i] = C.INDEX_UNSET;\n      if (selections[i] != null) {\n        TrackGroup trackGroup = selections[i].getTrackGroup();\n        for (int j = 0; j < periods.length; j++) {\n          if (periods[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {\n            selectionChildIndices[i] = j;\n            break;\n          }\n        }\n      }\n    }\n    streamPeriodIndices.clear();\n    // Select tracks for each child, copying the resulting streams back into a new streams array.\n    @NullableType SampleStream[] newStreams = new SampleStream[selections.length];\n    @NullableType SampleStream[] childStreams = new SampleStream[selections.length];\n    @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];\n    ArrayList<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);\n    for (int i = 0; i < periods.length; i++) {\n      for (int j = 0; j < selections.length; j++) {\n        childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;\n        childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;\n      }\n      long selectPositionUs = periods[i].selectTracks(childSelections, mayRetainStreamFlags,\n          childStreams, streamResetFlags, positionUs);\n      if (i == 0) {\n        positionUs = selectPositionUs;\n      } else if (selectPositionUs != positionUs) {\n        throw new IllegalStateException(\"Children enabled at different positions.\");\n      }\n      boolean periodEnabled = false;\n      for (int j = 0; j < selections.length; j++) {\n        if (selectionChildIndices[j] == i) {\n          // Assert that the child provided a stream for the selection.\n          SampleStream childStream = Assertions.checkNotNull(childStreams[j]);\n          newStreams[j] = childStreams[j];\n          periodEnabled = true;\n          streamPeriodIndices.put(childStream, i);\n        } else if (streamChildIndices[j] == i) {\n          // Assert that the child cleared any previous stream.\n          Assertions.checkState(childStreams[j] == null);\n        }\n      }\n      if (periodEnabled) {\n        enabledPeriodsList.add(periods[i]);\n      }\n    }\n    // Copy the new streams back into the streams array.\n    System.arraycopy(newStreams, 0, streams, 0, newStreams.length);\n    // Update the local state.\n    enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];\n    enabledPeriodsList.toArray(enabledPeriods);\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods);\n    return positionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    for (MediaPeriod period : enabledPeriods) {\n      period.discardBuffer(positionUs, toKeyframe);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    compositeSequenceableLoader.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    if (!childrenPendingPreparation.isEmpty()) {\n      // Preparation is still going on.\n      int childrenPendingPreparationSize = childrenPendingPreparation.size();\n      for (int i = 0; i < childrenPendingPreparationSize; i++) {\n        childrenPendingPreparation.get(i).continueLoading(positionUs);\n      }\n      return false;\n    } else {\n      return compositeSequenceableLoader.continueLoading(positionUs);\n    }\n  }\n\n  @Override\n  public boolean isLoading() {\n    return compositeSequenceableLoader.isLoading();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return compositeSequenceableLoader.getNextLoadPositionUs();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    long positionUs = periods[0].readDiscontinuity();\n    // Periods other than the first one are not allowed to report discontinuities.\n    for (int i = 1; i < periods.length; i++) {\n      if (periods[i].readDiscontinuity() != C.TIME_UNSET) {\n        throw new IllegalStateException(\"Child reported discontinuity.\");\n      }\n    }\n    // It must be possible to seek enabled periods to the new position, if there is one.\n    if (positionUs != C.TIME_UNSET) {\n      for (MediaPeriod enabledPeriod : enabledPeriods) {\n        if (enabledPeriod != periods[0]\n            && enabledPeriod.seekToUs(positionUs) != positionUs) {\n          throw new IllegalStateException(\"Unexpected child seekToUs result.\");\n        }\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return compositeSequenceableLoader.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    positionUs = enabledPeriods[0].seekToUs(positionUs);\n    // Additional periods must seek to the same position.\n    for (int i = 1; i < enabledPeriods.length; i++) {\n      if (enabledPeriods[i].seekToUs(positionUs) != positionUs) {\n        throw new IllegalStateException(\"Unexpected child seekToUs result.\");\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0];\n    return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);\n  }\n\n  // MediaPeriod.Callback implementation\n\n  @Override\n  public void onPrepared(MediaPeriod preparedPeriod) {\n    childrenPendingPreparation.remove(preparedPeriod);\n    if (!childrenPendingPreparation.isEmpty()) {\n      return;\n    }\n    int totalTrackGroupCount = 0;\n    for (MediaPeriod period : periods) {\n      totalTrackGroupCount += period.getTrackGroups().length;\n    }\n    TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];\n    int trackGroupIndex = 0;\n    for (MediaPeriod period : periods) {\n      TrackGroupArray periodTrackGroups = period.getTrackGroups();\n      int periodTrackGroupCount = periodTrackGroups.length;\n      for (int j = 0; j < periodTrackGroupCount; j++) {\n        trackGroupArray[trackGroupIndex++] = periodTrackGroups.get(j);\n      }\n    }\n    trackGroups = new TrackGroupArray(trackGroupArray);\n    Assertions.checkNotNull(callback).onPrepared(this);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod ignored) {\n    Assertions.checkNotNull(callback).onContinueLoadingRequested(this);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\n\n/**\n * Merges multiple {@link MediaSource}s.\n *\n * <p>The {@link Timeline}s of the sources being merged must have the same number of periods.\n */\npublic final class MergingMediaSource extends CompositeMediaSource<Integer> {\n\n  /**\n   * Thrown when a {@link MergingMediaSource} cannot merge its sources.\n   */\n  public static final class IllegalMergeException extends IOException {\n\n    /** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({REASON_PERIOD_COUNT_MISMATCH})\n    public @interface Reason {}\n    /**\n     * The sources have different period counts.\n     */\n    public static final int REASON_PERIOD_COUNT_MISMATCH = 0;\n\n    /**\n     * The reason the merge failed.\n     */\n    @Reason public final int reason;\n\n    /**\n     * @param reason The reason the merge failed.\n     */\n    public IllegalMergeException(@Reason int reason) {\n      this.reason = reason;\n    }\n\n  }\n\n  private static final int PERIOD_COUNT_UNSET = -1;\n\n  private final MediaSource[] mediaSources;\n  private final Timeline[] timelines;\n  private final ArrayList<MediaSource> pendingTimelineSources;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n\n  private int periodCount;\n  @Nullable private IllegalMergeException mergeError;\n\n  /**\n   * @param mediaSources The {@link MediaSource}s to merge.\n   */\n  public MergingMediaSource(MediaSource... mediaSources) {\n    this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources);\n  }\n\n  /**\n   * @param compositeSequenceableLoaderFactory A factory to create composite\n   *     {@link SequenceableLoader}s for when this media source loads data from multiple streams\n   *     (video, audio etc...).\n   * @param mediaSources The {@link MediaSource}s to merge.\n   */\n  public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      MediaSource... mediaSources) {\n    this.mediaSources = mediaSources;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));\n    periodCount = PERIOD_COUNT_UNSET;\n    timelines = new Timeline[mediaSources.length];\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return mediaSources.length > 0 ? mediaSources[0].getTag() : null;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    for (int i = 0; i < mediaSources.length; i++) {\n      prepareChildSource(i, mediaSources[i]);\n    }\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    if (mergeError != null) {\n      throw mergeError;\n    }\n    super.maybeThrowSourceInfoRefreshError();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    MediaPeriod[] periods = new MediaPeriod[mediaSources.length];\n    int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);\n    for (int i = 0; i < periods.length; i++) {\n      MediaPeriodId childMediaPeriodId =\n          id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));\n      periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);\n    }\n    return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;\n    for (int i = 0; i < mediaSources.length; i++) {\n      mediaSources[i].releasePeriod(mergingPeriod.periods[i]);\n    }\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    Arrays.fill(timelines, null);\n    periodCount = PERIOD_COUNT_UNSET;\n    mergeError = null;\n    pendingTimelineSources.clear();\n    Collections.addAll(pendingTimelineSources, mediaSources);\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      Integer id, MediaSource mediaSource, Timeline timeline) {\n    if (mergeError == null) {\n      mergeError = checkTimelineMerges(timeline);\n    }\n    if (mergeError != null) {\n      return;\n    }\n    pendingTimelineSources.remove(mediaSource);\n    timelines[id] = timeline;\n    if (pendingTimelineSources.isEmpty()) {\n      refreshSourceInfo(timelines[0]);\n    }\n  }\n\n  @Override\n  @Nullable\n  protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      Integer id, MediaPeriodId mediaPeriodId) {\n    return id == 0 ? mediaPeriodId : null;\n  }\n\n  @Nullable\n  private IllegalMergeException checkTimelineMerges(Timeline timeline) {\n    if (periodCount == PERIOD_COUNT_UNSET) {\n      periodCount = timeline.getPeriodCount();\n    } else if (timeline.getPeriodCount() != periodCount) {\n      return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorInput;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;\nimport com.google.android.exoplayer2.extractor.SeekMap.Unseekable;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.icy.IcyHeaders;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport com.google.android.exoplayer2.upstream.StatsDataSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ConditionVariable;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */\n/* package */ final class ProgressiveMediaPeriod\n    implements MediaPeriod,\n        ExtractorOutput,\n        Loader.Callback<ProgressiveMediaPeriod.ExtractingLoadable>,\n        Loader.ReleaseCallback,\n        UpstreamFormatChangedListener {\n\n  /**\n   * Listener for information about the period.\n   */\n  interface Listener {\n\n    /**\n     * Called when the duration, the ability to seek within the period, or the categorization as\n     * live stream changes.\n     *\n     * @param durationUs The duration of the period, or {@link C#TIME_UNSET}.\n     * @param isSeekable Whether the period is seekable.\n     * @param isLive Whether the period is live.\n     */\n    void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive);\n  }\n\n  /**\n   * When the source's duration is unknown, it is calculated by adding this value to the largest\n   * sample timestamp seen when buffering completes.\n   */\n  private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;\n\n  private static final Map<String, String> ICY_METADATA_HEADERS = createIcyMetadataHeaders();\n\n  private static final Format ICY_FORMAT =\n      Format.createSampleFormat(\"icy\", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE);\n\n  private final Uri uri;\n  private final DataSource dataSource;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final EventDispatcher eventDispatcher;\n  private final Listener listener;\n  private final Allocator allocator;\n  @Nullable private final String customCacheKey;\n  private final long continueLoadingCheckIntervalBytes;\n  private final Loader loader;\n  private final ExtractorHolder extractorHolder;\n  private final ConditionVariable loadCondition;\n  private final Runnable maybeFinishPrepareRunnable;\n  private final Runnable onContinueLoadingRequestedRunnable;\n  private final Handler handler;\n\n  @Nullable private Callback callback;\n  @Nullable private SeekMap seekMap;\n  @Nullable private IcyHeaders icyHeaders;\n  private SampleQueue[] sampleQueues;\n  private TrackId[] sampleQueueTrackIds;\n  private boolean sampleQueuesBuilt;\n  private boolean prepared;\n\n  @Nullable private PreparedState preparedState;\n  private boolean haveAudioVideoTracks;\n  private int dataType;\n\n  private boolean seenFirstTrackSelection;\n  private boolean notifyDiscontinuity;\n  private boolean notifiedReadingStarted;\n  private int enabledTrackCount;\n  private long durationUs;\n  private long length;\n  private boolean isLive;\n\n  private long lastSeekPositionUs;\n  private long pendingResetPositionUs;\n  private boolean pendingDeferredRetry;\n\n  private int extractedSamplesCountAtStartOfLoad;\n  private boolean loadingFinished;\n  private boolean released;\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSource The data source to read the media.\n   * @param extractors The extractors to use to read the data source.\n   * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.\n   * @param eventDispatcher A dispatcher to notify of events.\n   * @param listener A listener to notify when information about the period changes.\n   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.\n   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache\n   *     indexing. May be null.\n   * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each\n   *     invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.\n   */\n  // maybeFinishPrepare is not posted to the handler until initialization completes.\n  @SuppressWarnings({\n    \"nullness:argument.type.incompatible\",\n    \"nullness:methodref.receiver.bound.invalid\"\n  })\n  public ProgressiveMediaPeriod(\n      Uri uri,\n      DataSource dataSource,\n      Extractor[] extractors,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      Listener listener,\n      Allocator allocator,\n      @Nullable String customCacheKey,\n      int continueLoadingCheckIntervalBytes) {\n    this.uri = uri;\n    this.dataSource = dataSource;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.listener = listener;\n    this.allocator = allocator;\n    this.customCacheKey = customCacheKey;\n    this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;\n    loader = new Loader(\"Loader:ProgressiveMediaPeriod\");\n    extractorHolder = new ExtractorHolder(extractors);\n    loadCondition = new ConditionVariable();\n    maybeFinishPrepareRunnable = this::maybeFinishPrepare;\n    onContinueLoadingRequestedRunnable =\n        () -> {\n          if (!released) {\n            Assertions.checkNotNull(callback)\n                .onContinueLoadingRequested(ProgressiveMediaPeriod.this);\n          }\n        };\n    handler = new Handler();\n    sampleQueueTrackIds = new TrackId[0];\n    sampleQueues = new SampleQueue[0];\n    pendingResetPositionUs = C.TIME_UNSET;\n    length = C.LENGTH_UNSET;\n    durationUs = C.TIME_UNSET;\n    dataType = C.DATA_TYPE_MEDIA;\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  public void release() {\n    if (prepared) {\n      // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise\n      // sampleQueues may still be being modified by the loading thread.\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.preRelease();\n      }\n    }\n    loader.release(/* callback= */ this);\n    handler.removeCallbacksAndMessages(null);\n    callback = null;\n    released = true;\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  @Override\n  public void onLoaderReleased() {\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.release();\n    }\n    extractorHolder.release();\n  }\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    loadCondition.open();\n    startLoading();\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    maybeThrowError();\n    if (loadingFinished && !prepared) {\n      throw new ParserException(\"Loading finished before preparation is complete.\");\n    }\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return getPreparedState().tracks;\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    PreparedState preparedState = getPreparedState();\n    TrackGroupArray tracks = preparedState.tracks;\n    boolean[] trackEnabledStates = preparedState.trackEnabledStates;\n    int oldEnabledTrackCount = enabledTrackCount;\n    // Deselect old tracks.\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n        int track = ((SampleStreamImpl) streams[i]).track;\n        Assertions.checkState(trackEnabledStates[track]);\n        enabledTrackCount--;\n        trackEnabledStates[track] = false;\n        streams[i] = null;\n      }\n    }\n    // We'll always need to seek if this is a first selection to a non-zero position, or if we're\n    // making a selection having previously disabled all tracks.\n    boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0;\n    // Select new tracks.\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] == null && selections[i] != null) {\n        TrackSelection selection = selections[i];\n        Assertions.checkState(selection.length() == 1);\n        Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);\n        int track = tracks.indexOf(selection.getTrackGroup());\n        Assertions.checkState(!trackEnabledStates[track]);\n        enabledTrackCount++;\n        trackEnabledStates[track] = true;\n        streams[i] = new SampleStreamImpl(track);\n        streamResetFlags[i] = true;\n        // If there's still a chance of avoiding a seek, try and seek within the sample queue.\n        if (!seekRequired) {\n          SampleQueue sampleQueue = sampleQueues[track];\n          sampleQueue.rewind();\n          // A seek can be avoided if we're able to advance to the current playback position in the\n          // sample queue, or if we haven't read anything from the queue since the previous seek\n          // (this case is common for sparse tracks such as metadata tracks). In all other cases a\n          // seek is required.\n          seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED\n              && sampleQueue.getReadIndex() != 0;\n        }\n      }\n    }\n    if (enabledTrackCount == 0) {\n      pendingDeferredRetry = false;\n      notifyDiscontinuity = false;\n      if (loader.isLoading()) {\n        // Discard as much as we can synchronously.\n        for (SampleQueue sampleQueue : sampleQueues) {\n          sampleQueue.discardToEnd();\n        }\n        loader.cancelLoading();\n      } else {\n        for (SampleQueue sampleQueue : sampleQueues) {\n          sampleQueue.reset();\n        }\n      }\n    } else if (seekRequired) {\n      positionUs = seekToUs(positionUs);\n      // We'll need to reset renderers consuming from all streams due to the seek.\n      for (int i = 0; i < streams.length; i++) {\n        if (streams[i] != null) {\n          streamResetFlags[i] = true;\n        }\n      }\n    }\n    seenFirstTrackSelection = true;\n    return positionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    if (isPendingReset()) {\n      return;\n    }\n    boolean[] trackEnabledStates = getPreparedState().trackEnabledStates;\n    int trackCount = sampleQueues.length;\n    for (int i = 0; i < trackCount; i++) {\n      sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    // Do nothing.\n  }\n\n  @Override\n  public boolean continueLoading(long playbackPositionUs) {\n    if (loadingFinished\n        || loader.hasFatalError()\n        || pendingDeferredRetry\n        || (prepared && enabledTrackCount == 0)) {\n      return false;\n    }\n    boolean continuedLoading = loadCondition.open();\n    if (!loader.isLoading()) {\n      startLoading();\n      continuedLoading = true;\n    }\n    return continuedLoading;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return loader.isLoading() && loadCondition.isOpen();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    if (notifyDiscontinuity\n        && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {\n      notifyDiscontinuity = false;\n      return lastSeekPositionUs;\n    }\n    return C.TIME_UNSET;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags;\n    if (loadingFinished) {\n      return C.TIME_END_OF_SOURCE;\n    } else if (isPendingReset()) {\n      return pendingResetPositionUs;\n    }\n    long largestQueuedTimestampUs = Long.MAX_VALUE;\n    if (haveAudioVideoTracks) {\n      // Ignore non-AV tracks, which may be sparse or poorly interleaved.\n      int trackCount = sampleQueues.length;\n      for (int i = 0; i < trackCount; i++) {\n        if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {\n          largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,\n              sampleQueues[i].getLargestQueuedTimestampUs());\n        }\n      }\n    }\n    if (largestQueuedTimestampUs == Long.MAX_VALUE) {\n      largestQueuedTimestampUs = getLargestQueuedTimestampUs();\n    }\n    return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs\n        : largestQueuedTimestampUs;\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    PreparedState preparedState = getPreparedState();\n    SeekMap seekMap = preparedState.seekMap;\n    boolean[] trackIsAudioVideoFlags = preparedState.trackIsAudioVideoFlags;\n    // Treat all seeks into non-seekable media as being to t=0.\n    positionUs = seekMap.isSeekable() ? positionUs : 0;\n\n    notifyDiscontinuity = false;\n    lastSeekPositionUs = positionUs;\n    if (isPendingReset()) {\n      // A reset is already pending. We only need to update its position.\n      pendingResetPositionUs = positionUs;\n      return positionUs;\n    }\n\n    // If we're not playing a live stream, try and seek within the buffer.\n    if (dataType != C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE\n        && seekInsideBufferUs(trackIsAudioVideoFlags, positionUs)) {\n      return positionUs;\n    }\n\n    // We can't seek inside the buffer, and so need to reset.\n    pendingDeferredRetry = false;\n    pendingResetPositionUs = positionUs;\n    loadingFinished = false;\n    if (loader.isLoading()) {\n      loader.cancelLoading();\n    } else {\n      loader.clearFatalError();\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.reset();\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    SeekMap seekMap = getPreparedState().seekMap;\n    if (!seekMap.isSeekable()) {\n      // Treat all seeks into non-seekable media as being to t=0.\n      return 0;\n    }\n    SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);\n    return Util.resolveSeekPositionUs(\n        positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);\n  }\n\n  // SampleStream methods.\n\n  /* package */ boolean isReady(int track) {\n    return !suppressRead() && sampleQueues[track].isReady(loadingFinished);\n  }\n\n  /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException {\n    sampleQueues[sampleQueueIndex].maybeThrowError();\n    maybeThrowError();\n  }\n\n  /* package */ void maybeThrowError() throws IOException {\n    loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));\n  }\n\n  /* package */ int readData(\n      int sampleQueueIndex,\n      FormatHolder formatHolder,\n      DecoderInputBuffer buffer,\n      boolean formatRequired) {\n    if (suppressRead()) {\n      return C.RESULT_NOTHING_READ;\n    }\n    maybeNotifyDownstreamFormat(sampleQueueIndex);\n    int result =\n        sampleQueues[sampleQueueIndex].read(\n            formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);\n    if (result == C.RESULT_NOTHING_READ) {\n      maybeStartDeferredRetry(sampleQueueIndex);\n    }\n    return result;\n  }\n\n  /* package */ int skipData(int track, long positionUs) {\n    if (suppressRead()) {\n      return 0;\n    }\n    maybeNotifyDownstreamFormat(track);\n    SampleQueue sampleQueue = sampleQueues[track];\n    int skipCount;\n    if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {\n      skipCount = sampleQueue.advanceToEnd();\n    } else {\n      skipCount = sampleQueue.advanceTo(positionUs, true, true);\n      if (skipCount == SampleQueue.ADVANCE_FAILED) {\n        skipCount = 0;\n      }\n    }\n    if (skipCount == 0) {\n      maybeStartDeferredRetry(track);\n    }\n    return skipCount;\n  }\n\n  private void maybeNotifyDownstreamFormat(int track) {\n    PreparedState preparedState = getPreparedState();\n    boolean[] trackNotifiedDownstreamFormats = preparedState.trackNotifiedDownstreamFormats;\n    if (!trackNotifiedDownstreamFormats[track]) {\n      Format trackFormat = preparedState.tracks.get(track).getFormat(/* index= */ 0);\n      eventDispatcher.downstreamFormatChanged(\n          MimeTypes.getTrackType(trackFormat.sampleMimeType),\n          trackFormat,\n          C.SELECTION_REASON_UNKNOWN,\n          /* trackSelectionData= */ null,\n          lastSeekPositionUs);\n      trackNotifiedDownstreamFormats[track] = true;\n    }\n  }\n\n  private void maybeStartDeferredRetry(int track) {\n    boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags;\n    if (!pendingDeferredRetry\n        || !trackIsAudioVideoFlags[track]\n        || sampleQueues[track].isReady(/* loadingFinished= */ false)) {\n      return;\n    }\n    pendingResetPositionUs = 0;\n    pendingDeferredRetry = false;\n    notifyDiscontinuity = true;\n    lastSeekPositionUs = 0;\n    extractedSamplesCountAtStartOfLoad = 0;\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.reset();\n    }\n    Assertions.checkNotNull(callback).onContinueLoadingRequested(this);\n  }\n\n  private boolean suppressRead() {\n    return notifyDiscontinuity || isPendingReset();\n  }\n\n  // Loader.Callback implementation.\n\n  @Override\n  public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,\n      long loadDurationMs) {\n    if (durationUs == C.TIME_UNSET && seekMap != null) {\n      boolean isSeekable = seekMap.isSeekable();\n      long largestQueuedTimestampUs = getLargestQueuedTimestampUs();\n      durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0\n          : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;\n      listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive);\n    }\n    eventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        /* trackFormat= */ null,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ loadable.seekTimeUs,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.dataSource.getBytesRead());\n    copyLengthFromLoader(loadable);\n    loadingFinished = true;\n    Assertions.checkNotNull(callback).onContinueLoadingRequested(this);\n  }\n\n  @Override\n  public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,\n      long loadDurationMs, boolean released) {\n    eventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        /* trackFormat= */ null,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ loadable.seekTimeUs,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.dataSource.getBytesRead());\n    if (!released) {\n      copyLengthFromLoader(loadable);\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.reset();\n      }\n      if (enabledTrackCount > 0) {\n        Assertions.checkNotNull(callback).onContinueLoadingRequested(this);\n      }\n    }\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      ExtractingLoadable loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    copyLengthFromLoader(loadable);\n    LoadErrorAction loadErrorAction;\n    long retryDelayMs =\n        loadErrorHandlingPolicy.getRetryDelayMsFor(dataType, loadDurationMs, error, errorCount);\n    if (retryDelayMs == C.TIME_UNSET) {\n      loadErrorAction = Loader.DONT_RETRY_FATAL;\n    } else /* the load should be retried */ {\n      int extractedSamplesCount = getExtractedSamplesCount();\n      boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;\n      loadErrorAction =\n          configureRetry(loadable, extractedSamplesCount)\n              ? Loader.createRetryAction(/* resetErrorCount= */ madeProgress, retryDelayMs)\n              : Loader.DONT_RETRY;\n    }\n\n    eventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        /* trackFormat= */ null,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ loadable.seekTimeUs,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.dataSource.getBytesRead(),\n        error,\n        !loadErrorAction.isRetry());\n    return loadErrorAction;\n  }\n\n  // ExtractorOutput implementation. Called by the loading thread.\n\n  @Override\n  public TrackOutput track(int id, int type) {\n    return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false));\n  }\n\n  @Override\n  public void endTracks() {\n    sampleQueuesBuilt = true;\n    handler.post(maybeFinishPrepareRunnable);\n  }\n\n  @Override\n  public void seekMap(SeekMap seekMap) {\n    this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs */ C.TIME_UNSET);\n    handler.post(maybeFinishPrepareRunnable);\n  }\n\n  // Icy metadata. Called by the loading thread.\n\n  /* package */ TrackOutput icyTrack() {\n    return prepareTrackOutput(new TrackId(0, /* isIcyTrack= */ true));\n  }\n\n  // UpstreamFormatChangedListener implementation. Called by the loading thread.\n\n  @Override\n  public void onUpstreamFormatChanged(Format format) {\n    handler.post(maybeFinishPrepareRunnable);\n  }\n\n  // Internal methods.\n\n  private TrackOutput prepareTrackOutput(TrackId id) {\n    int trackCount = sampleQueues.length;\n    for (int i = 0; i < trackCount; i++) {\n      if (id.equals(sampleQueueTrackIds[i])) {\n        return sampleQueues[i];\n      }\n    }\n    SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager);\n    trackOutput.setUpstreamFormatChangeListener(this);\n    @NullableType\n    TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);\n    sampleQueueTrackIds[trackCount] = id;\n    this.sampleQueueTrackIds = Util.castNonNullTypeArray(sampleQueueTrackIds);\n    @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);\n    sampleQueues[trackCount] = trackOutput;\n    this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);\n    return trackOutput;\n  }\n\n  private void maybeFinishPrepare() {\n    SeekMap seekMap = this.seekMap;\n    if (released || prepared || !sampleQueuesBuilt || seekMap == null) {\n      return;\n    }\n    for (SampleQueue sampleQueue : sampleQueues) {\n      if (sampleQueue.getUpstreamFormat() == null) {\n        return;\n      }\n    }\n    loadCondition.close();\n    int trackCount = sampleQueues.length;\n    TrackGroup[] trackArray = new TrackGroup[trackCount];\n    boolean[] trackIsAudioVideoFlags = new boolean[trackCount];\n    durationUs = seekMap.getDurationUs();\n    for (int i = 0; i < trackCount; i++) {\n      Format trackFormat = sampleQueues[i].getUpstreamFormat();\n      String mimeType = trackFormat.sampleMimeType;\n      boolean isAudio = MimeTypes.isAudio(mimeType);\n      boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);\n      trackIsAudioVideoFlags[i] = isAudioVideo;\n      haveAudioVideoTracks |= isAudioVideo;\n      IcyHeaders icyHeaders = this.icyHeaders;\n      if (icyHeaders != null) {\n        if (isAudio || sampleQueueTrackIds[i].isIcyTrack) {\n          Metadata metadata = trackFormat.metadata;\n          trackFormat =\n              trackFormat.copyWithMetadata(\n                  metadata == null\n                      ? new Metadata(icyHeaders)\n                      : metadata.copyWithAppendedEntries(icyHeaders));\n        }\n        if (isAudio\n            && trackFormat.bitrate == Format.NO_VALUE\n            && icyHeaders.bitrate != Format.NO_VALUE) {\n          trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate);\n        }\n      }\n      trackArray[i] = new TrackGroup(trackFormat);\n    }\n    isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET;\n    dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA;\n    preparedState =\n        new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags);\n    prepared = true;\n    listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive);\n    Assertions.checkNotNull(callback).onPrepared(this);\n  }\n\n  private PreparedState getPreparedState() {\n    return Assertions.checkNotNull(preparedState);\n  }\n\n  private void copyLengthFromLoader(ExtractingLoadable loadable) {\n    if (length == C.LENGTH_UNSET) {\n      length = loadable.length;\n    }\n  }\n\n  private void startLoading() {\n    ExtractingLoadable loadable =\n        new ExtractingLoadable(\n            uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition);\n    if (prepared) {\n      SeekMap seekMap = getPreparedState().seekMap;\n      Assertions.checkState(isPendingReset());\n      if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) {\n        loadingFinished = true;\n        pendingResetPositionUs = C.TIME_UNSET;\n        return;\n      }\n      loadable.setLoadPosition(\n          seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs);\n      pendingResetPositionUs = C.TIME_UNSET;\n    }\n    extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();\n    long elapsedRealtimeMs =\n        loader.startLoading(\n            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));\n    eventDispatcher.loadStarted(\n        loadable.dataSpec,\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        /* trackFormat= */ null,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ loadable.seekTimeUs,\n        durationUs,\n        elapsedRealtimeMs);\n  }\n\n  /**\n   * Called to configure a retry when a load error occurs.\n   *\n   * @param loadable The current loadable for which the error was encountered.\n   * @param currentExtractedSampleCount The current number of samples that have been extracted into\n   *     the sample queues.\n   * @return Whether the loader should retry with the current loadable. False indicates a deferred\n   *     retry.\n   */\n  private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) {\n    if (length != C.LENGTH_UNSET\n        || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {\n      // We're playing an on-demand stream. Resume the current loadable, which will\n      // request data starting from the point it left off.\n      extractedSamplesCountAtStartOfLoad = currentExtractedSampleCount;\n      return true;\n    } else if (prepared && !suppressRead()) {\n      // We're playing a stream of unknown length and duration. Assume it's live, and therefore that\n      // the data at the uri is a continuously shifting window of the latest available media. For\n      // this case there's no way to continue loading from where a previous load finished, so it's\n      // necessary to load from the start whenever commencing a new load. Deferring the retry until\n      // we run out of buffered data makes for a much better user experience. See:\n      // https://github.com/google/ExoPlayer/issues/1606.\n      // Note that the suppressRead() check means only a single deferred retry can occur without\n      // progress being made. Any subsequent failures without progress will go through the else\n      // block below.\n      pendingDeferredRetry = true;\n      return false;\n    } else {\n      // This is the same case as above, except in this case there's no value in deferring the retry\n      // because there's no buffered data to be read. This case also covers an on-demand stream with\n      // unknown length that has yet to be prepared. This case cannot be disambiguated from the live\n      // stream case, so we have no option but to load from the start.\n      notifyDiscontinuity = prepared;\n      lastSeekPositionUs = 0;\n      extractedSamplesCountAtStartOfLoad = 0;\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.reset();\n      }\n      loadable.setLoadPosition(0, 0);\n      return true;\n    }\n  }\n\n  /**\n   * Attempts to seek to the specified position within the sample queues.\n   *\n   * @param trackIsAudioVideoFlags Whether each track is audio/video.\n   * @param positionUs The seek position in microseconds.\n   * @return Whether the in-buffer seek was successful.\n   */\n  private boolean seekInsideBufferUs(boolean[] trackIsAudioVideoFlags, long positionUs) {\n    int trackCount = sampleQueues.length;\n    for (int i = 0; i < trackCount; i++) {\n      SampleQueue sampleQueue = sampleQueues[i];\n      sampleQueue.rewind();\n      boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)\n          != SampleQueue.ADVANCE_FAILED;\n      // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue\n      // is successful. We ignore whether seeks within non-AV queues are successful in this case, as\n      // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is\n      // successful only if the seek into every queue succeeds.\n      if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private int getExtractedSamplesCount() {\n    int extractedSamplesCount = 0;\n    for (SampleQueue sampleQueue : sampleQueues) {\n      extractedSamplesCount += sampleQueue.getWriteIndex();\n    }\n    return extractedSamplesCount;\n  }\n\n  private long getLargestQueuedTimestampUs() {\n    long largestQueuedTimestampUs = Long.MIN_VALUE;\n    for (SampleQueue sampleQueue : sampleQueues) {\n      largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,\n          sampleQueue.getLargestQueuedTimestampUs());\n    }\n    return largestQueuedTimestampUs;\n  }\n\n  private boolean isPendingReset() {\n    return pendingResetPositionUs != C.TIME_UNSET;\n  }\n\n  private final class SampleStreamImpl implements SampleStream {\n\n    private final int track;\n\n    public SampleStreamImpl(int track) {\n      this.track = track;\n    }\n\n    @Override\n    public boolean isReady() {\n      return ProgressiveMediaPeriod.this.isReady(track);\n    }\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      ProgressiveMediaPeriod.this.maybeThrowError(track);\n    }\n\n    @Override\n    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n        boolean formatRequired) {\n      return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);\n    }\n\n    @Override\n    public int skipData(long positionUs) {\n      return ProgressiveMediaPeriod.this.skipData(track, positionUs);\n    }\n\n  }\n\n  /** Loads the media stream and extracts sample data from it. */\n  /* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener {\n\n    private final Uri uri;\n    private final StatsDataSource dataSource;\n    private final ExtractorHolder extractorHolder;\n    private final ExtractorOutput extractorOutput;\n    private final ConditionVariable loadCondition;\n    private final PositionHolder positionHolder;\n\n    private volatile boolean loadCanceled;\n\n    private boolean pendingExtractorSeek;\n    private long seekTimeUs;\n    private DataSpec dataSpec;\n    private long length;\n    @Nullable private TrackOutput icyTrackOutput;\n    private boolean seenIcyMetadata;\n\n    @SuppressWarnings(\"method.invocation.invalid\")\n    public ExtractingLoadable(\n        Uri uri,\n        DataSource dataSource,\n        ExtractorHolder extractorHolder,\n        ExtractorOutput extractorOutput,\n        ConditionVariable loadCondition) {\n      this.uri = uri;\n      this.dataSource = new StatsDataSource(dataSource);\n      this.extractorHolder = extractorHolder;\n      this.extractorOutput = extractorOutput;\n      this.loadCondition = loadCondition;\n      this.positionHolder = new PositionHolder();\n      this.pendingExtractorSeek = true;\n      this.length = C.LENGTH_UNSET;\n      dataSpec = buildDataSpec(/* position= */ 0);\n    }\n\n    // Loadable implementation.\n\n    @Override\n    public void cancelLoad() {\n      loadCanceled = true;\n    }\n\n    @Override\n    public void load() throws IOException, InterruptedException {\n      int result = Extractor.RESULT_CONTINUE;\n      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {\n        ExtractorInput input = null;\n        try {\n          long position = positionHolder.position;\n          dataSpec = buildDataSpec(position);\n          length = dataSource.open(dataSpec);\n          if (length != C.LENGTH_UNSET) {\n            length += position;\n          }\n          Uri uri = Assertions.checkNotNull(dataSource.getUri());\n          icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders());\n          DataSource extractorDataSource = dataSource;\n          if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) {\n            extractorDataSource = new IcyDataSource(dataSource, icyHeaders.metadataInterval, this);\n            icyTrackOutput = icyTrack();\n            icyTrackOutput.format(ICY_FORMAT);\n          }\n          input = new DefaultExtractorInput(extractorDataSource, position, length);\n          Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri);\n\n          // MP3 live streams commonly have seekable metadata, despite being unseekable.\n          if (icyHeaders != null && extractor instanceof Mp3Extractor) {\n            ((Mp3Extractor) extractor).disableSeeking();\n          }\n\n          if (pendingExtractorSeek) {\n            extractor.seek(position, seekTimeUs);\n            pendingExtractorSeek = false;\n          }\n          while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {\n            loadCondition.block();\n            result = extractor.read(input, positionHolder);\n            if (input.getPosition() > position + continueLoadingCheckIntervalBytes) {\n              position = input.getPosition();\n              loadCondition.close();\n              handler.post(onContinueLoadingRequestedRunnable);\n            }\n          }\n        } finally {\n          if (result == Extractor.RESULT_SEEK) {\n            result = Extractor.RESULT_CONTINUE;\n          } else if (input != null) {\n            positionHolder.position = input.getPosition();\n          }\n          Util.closeQuietly(dataSource);\n        }\n      }\n    }\n\n    // IcyDataSource.Listener\n\n    @Override\n    public void onIcyMetadata(ParsableByteArray metadata) {\n      // Always output the first ICY metadata at the start time. This helps minimize any delay\n      // between the start of playback and the first ICY metadata event.\n      long timeUs =\n          !seenIcyMetadata ? seekTimeUs : Math.max(getLargestQueuedTimestampUs(), seekTimeUs);\n      int length = metadata.bytesLeft();\n      TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput);\n      icyTrackOutput.sampleData(metadata, length);\n      icyTrackOutput.sampleMetadata(\n          timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* encryptionData= */ null);\n      seenIcyMetadata = true;\n    }\n\n    // Internal methods.\n\n    private DataSpec buildDataSpec(long position) {\n      // Disable caching if the content length cannot be resolved, since this is indicative of a\n      // progressive live stream.\n      return new DataSpec(\n          uri,\n          position,\n          C.LENGTH_UNSET,\n          customCacheKey,\n          DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION,\n          ICY_METADATA_HEADERS);\n    }\n\n    private void setLoadPosition(long position, long timeUs) {\n      positionHolder.position = position;\n      seekTimeUs = timeUs;\n      pendingExtractorSeek = true;\n      seenIcyMetadata = false;\n    }\n  }\n\n  /** Stores a list of extractors and a selected extractor when the format has been detected. */\n  private static final class ExtractorHolder {\n\n    private final Extractor[] extractors;\n\n    @Nullable private Extractor extractor;\n\n    /**\n     * Creates a holder that will select an extractor and initialize it using the specified output.\n     *\n     * @param extractors One or more extractors to choose from.\n     */\n    public ExtractorHolder(Extractor[] extractors) {\n      this.extractors = extractors;\n    }\n\n    /**\n     * Returns an initialized extractor for reading {@code input}, and returns the same extractor on\n     * later calls.\n     *\n     * @param input The {@link ExtractorInput} from which data should be read.\n     * @param output The {@link ExtractorOutput} that will be used to initialize the selected\n     *     extractor.\n     * @param uri The {@link Uri} of the data.\n     * @return An initialized extractor for reading {@code input}.\n     * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.\n     * @throws IOException Thrown if the input could not be read.\n     * @throws InterruptedException Thrown if the thread was interrupted.\n     */\n    public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, Uri uri)\n        throws IOException, InterruptedException {\n      if (extractor != null) {\n        return extractor;\n      }\n      if (extractors.length == 1) {\n        this.extractor = extractors[0];\n      } else {\n        for (Extractor extractor : extractors) {\n          try {\n            if (extractor.sniff(input)) {\n              this.extractor = extractor;\n              break;\n            }\n          } catch (EOFException e) {\n            // Do nothing.\n          } finally {\n            input.resetPeekPosition();\n          }\n        }\n        if (extractor == null) {\n          throw new UnrecognizedInputFormatException(\n              \"None of the available extractors (\"\n                  + Util.getCommaDelimitedSimpleClassNames(extractors)\n                  + \") could read the stream.\",\n              uri);\n        }\n      }\n      extractor.init(output);\n      return extractor;\n    }\n\n    public void release() {\n      if (extractor != null) {\n        extractor.release();\n        extractor = null;\n      }\n    }\n  }\n\n  /** Stores state that is initialized when preparation completes. */\n  private static final class PreparedState {\n\n    public final SeekMap seekMap;\n    public final TrackGroupArray tracks;\n    public final boolean[] trackIsAudioVideoFlags;\n    public final boolean[] trackEnabledStates;\n    public final boolean[] trackNotifiedDownstreamFormats;\n\n    public PreparedState(\n        SeekMap seekMap, TrackGroupArray tracks, boolean[] trackIsAudioVideoFlags) {\n      this.seekMap = seekMap;\n      this.tracks = tracks;\n      this.trackIsAudioVideoFlags = trackIsAudioVideoFlags;\n      this.trackEnabledStates = new boolean[tracks.length];\n      this.trackNotifiedDownstreamFormats = new boolean[tracks.length];\n    }\n  }\n\n  /** Identifies a track. */\n  private static final class TrackId {\n\n    public final int id;\n    public final boolean isIcyTrack;\n\n    public TrackId(int id, boolean isIcyTrack) {\n      this.id = id;\n      this.isIcyTrack = isIcyTrack;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      TrackId other = (TrackId) obj;\n      return id == other.id && isIcyTrack == other.isIcyTrack;\n    }\n\n    @Override\n    public int hashCode() {\n      return 31 * id + (isIcyTrack ? 1 : 0);\n    }\n  }\n\n  private static Map<String, String> createIcyMetadataHeaders() {\n    Map<String, String> headers = new HashMap<>();\n    headers.put(\n        IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,\n        IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);\n    return Collections.unmodifiableMap(headers);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\n\n/**\n * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.\n *\n * <p>If the possible input stream container formats are known, pass a factory that instantiates\n * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use\n * the default extractors. When reading a new stream, the first {@link Extractor} in the array of\n * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be\n * used to extract samples from the input stream.\n *\n * <p>Note that the built-in extractor for FLV streams does not support seeking.\n */\npublic final class ProgressiveMediaSource extends BaseMediaSource\n    implements ProgressiveMediaPeriod.Listener {\n\n  /** Factory for {@link ProgressiveMediaSource}s. */\n  public static final class Factory implements MediaSourceFactory {\n\n    private final DataSource.Factory dataSourceFactory;\n\n    private ExtractorsFactory extractorsFactory;\n    @Nullable private String customCacheKey;\n    @Nullable private Object tag;\n    private DrmSessionManager<?> drmSessionManager;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private int continueLoadingCheckIntervalBytes;\n    private boolean isCreateCalled;\n\n    /**\n     * Creates a new factory for {@link ProgressiveMediaSource}s, using the extractors provided by\n     * {@link DefaultExtractorsFactory}.\n     *\n     * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this(dataSourceFactory, new DefaultExtractorsFactory());\n    }\n\n    /**\n     * Creates a new factory for {@link ProgressiveMediaSource}s.\n     *\n     * @param dataSourceFactory A factory for {@link DataSource}s to read the media.\n     * @param extractorsFactory A factory for extractors used to extract media from its container.\n     */\n    public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {\n      this.dataSourceFactory = dataSourceFactory;\n      this.extractorsFactory = extractorsFactory;\n      drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;\n    }\n\n    /**\n     * Sets the factory for {@link Extractor}s to process the media stream. The default value is an\n     * instance of {@link DefaultExtractorsFactory}.\n     *\n     * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the\n     *     possible formats are known, pass a factory that instantiates extractors for those\n     *     formats.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n     * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory,\n     *     ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors\n     *     factory as unused.\n     */\n    @Deprecated\n    public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.extractorsFactory = extractorsFactory;\n      return this;\n    }\n\n    /**\n     * Sets the custom key that uniquely identifies the original stream. Used for cache indexing.\n     * The default value is {@code null}.\n     *\n     * @param customCacheKey A custom key that uniquely identifies the original stream. Used for\n     *     cache indexing.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n     */\n    public Factory setCustomCacheKey(String customCacheKey) {\n      Assertions.checkState(!isCreateCalled);\n      this.customCacheKey = customCacheKey;\n      return this;\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link\n     * com.google.android.exoplayer2.Timeline} of the source as {@link\n     * com.google.android.exoplayer2.Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n     */\n    public Factory setTag(Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The\n     * default value is {@link DrmSessionManager#DUMMY}.\n     *\n     * @param drmSessionManager The {@link DrmSessionManager}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {\n      Assertions.checkState(!isCreateCalled);\n      this.drmSessionManager = drmSessionManager;\n      return this;\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /**\n     * Sets the number of bytes that should be loaded between each invocation of {@link\n     * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is\n     * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.\n     *\n     * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between\n     *     each invocation of {@link\n     *     MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called.\n     */\n    public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {\n      Assertions.checkState(!isCreateCalled);\n      this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;\n      return this;\n    }\n\n    /**\n     * Returns a new {@link ProgressiveMediaSource} using the current parameters.\n     *\n     * @param uri The {@link Uri}.\n     * @return The new {@link ProgressiveMediaSource}.\n     */\n    @Override\n    public ProgressiveMediaSource createMediaSource(Uri uri) {\n      isCreateCalled = true;\n      return new ProgressiveMediaSource(\n          uri,\n          dataSourceFactory,\n          extractorsFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          customCacheKey,\n          continueLoadingCheckIntervalBytes,\n          tag);\n    }\n\n    @Override\n    public int[] getSupportedTypes() {\n      return new int[] {C.TYPE_OTHER};\n    }\n  }\n\n  /**\n   * The default number of bytes that should be loaded between each each invocation of {@link\n   * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.\n   */\n  public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;\n\n  private final Uri uri;\n  private final DataSource.Factory dataSourceFactory;\n  private final ExtractorsFactory extractorsFactory;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;\n  @Nullable private final String customCacheKey;\n  private final int continueLoadingCheckIntervalBytes;\n  @Nullable private final Object tag;\n\n  private long timelineDurationUs;\n  private boolean timelineIsSeekable;\n  private boolean timelineIsLive;\n  @Nullable private TransferListener transferListener;\n\n  // TODO: Make private when ExtractorMediaSource is deleted.\n  /* package */ ProgressiveMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      ExtractorsFactory extractorsFactory,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,\n      @Nullable String customCacheKey,\n      int continueLoadingCheckIntervalBytes,\n      @Nullable Object tag) {\n    this.uri = uri;\n    this.dataSourceFactory = dataSourceFactory;\n    this.extractorsFactory = extractorsFactory;\n    this.drmSessionManager = drmSessionManager;\n    this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;\n    this.customCacheKey = customCacheKey;\n    this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;\n    this.timelineDurationUs = C.TIME_UNSET;\n    this.tag = tag;\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    transferListener = mediaTransferListener;\n    drmSessionManager.prepare();\n    notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive);\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    DataSource dataSource = dataSourceFactory.createDataSource();\n    if (transferListener != null) {\n      dataSource.addTransferListener(transferListener);\n    }\n    return new ProgressiveMediaPeriod(\n        uri,\n        dataSource,\n        extractorsFactory.createExtractors(),\n        drmSessionManager,\n        loadableLoadErrorHandlingPolicy,\n        createEventDispatcher(id),\n        this,\n        allocator,\n        customCacheKey,\n        continueLoadingCheckIntervalBytes);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    ((ProgressiveMediaPeriod) mediaPeriod).release();\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    drmSessionManager.release();\n  }\n\n  // ProgressiveMediaPeriod.Listener implementation.\n\n  @Override\n  public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) {\n    // If we already have the duration from a previous source info refresh, use it.\n    durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;\n    if (timelineDurationUs == durationUs\n        && timelineIsSeekable == isSeekable\n        && timelineIsLive == isLive) {\n      // Suppress no-op source info changes.\n      return;\n    }\n    notifySourceInfoRefreshed(durationUs, isSeekable, isLive);\n  }\n\n  // Internal methods.\n\n  private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) {\n    timelineDurationUs = durationUs;\n    timelineIsSeekable = isSeekable;\n    timelineIsLive = isLive;\n    // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then\n    // indicate that the duration may change until it's known. See [internal: b/69703223].\n    refreshSourceInfo(\n        new SinglePeriodTimeline(\n            timelineDurationUs,\n            timelineIsSeekable,\n            /* isDynamic= */ false,\n            /* isLive= */ timelineIsLive,\n            /* manifest= */ null,\n            tag));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A queue of metadata describing the contents of a media buffer.\n */\n/* package */ final class SampleMetadataQueue {\n\n  /**\n   * A holder for sample metadata not held by {@link DecoderInputBuffer}.\n   */\n  public static final class SampleExtrasHolder {\n\n    public int size;\n    public long offset;\n    public CryptoData cryptoData;\n\n  }\n\n  private static final int SAMPLE_CAPACITY_INCREMENT = 1000;\n\n  private final DrmSessionManager<?> drmSessionManager;\n\n  @Nullable private Format downstreamFormat;\n  @Nullable private DrmSession<?> currentDrmSession;\n\n  private int capacity;\n  private int[] sourceIds;\n  private long[] offsets;\n  private int[] sizes;\n  private int[] flags;\n  private long[] timesUs;\n  private CryptoData[] cryptoDatas;\n  private Format[] formats;\n\n  private int length;\n  private int absoluteFirstIndex;\n  private int relativeFirstIndex;\n  private int readPosition;\n\n  private long largestDiscardedTimestampUs;\n  private long largestQueuedTimestampUs;\n  private boolean isLastSampleQueued;\n  private boolean upstreamKeyframeRequired;\n  private boolean upstreamFormatRequired;\n  private Format upstreamFormat;\n  private Format upstreamCommittedFormat;\n  private int upstreamSourceId;\n\n  public SampleMetadataQueue(DrmSessionManager<?> drmSessionManager) {\n    this.drmSessionManager = drmSessionManager;\n    capacity = SAMPLE_CAPACITY_INCREMENT;\n    sourceIds = new int[capacity];\n    offsets = new long[capacity];\n    timesUs = new long[capacity];\n    flags = new int[capacity];\n    sizes = new int[capacity];\n    cryptoDatas = new CryptoData[capacity];\n    formats = new Format[capacity];\n    largestDiscardedTimestampUs = Long.MIN_VALUE;\n    largestQueuedTimestampUs = Long.MIN_VALUE;\n    upstreamFormatRequired = true;\n    upstreamKeyframeRequired = true;\n  }\n\n  // Called by the consuming thread, but only when there is no loading thread.\n\n  /**\n   * Clears all sample metadata from the queue.\n   *\n   * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,\n   *     samples queued after the reset (and before a subsequent call to {@link #format(Format)})\n   *     are assumed to have the current upstream format. If set to true, {@link #format(Format)}\n   *     must be called after the reset before any more samples can be queued.\n   */\n  public void reset(boolean resetUpstreamFormat) {\n    length = 0;\n    absoluteFirstIndex = 0;\n    relativeFirstIndex = 0;\n    readPosition = 0;\n    upstreamKeyframeRequired = true;\n    largestDiscardedTimestampUs = Long.MIN_VALUE;\n    largestQueuedTimestampUs = Long.MIN_VALUE;\n    isLastSampleQueued = false;\n    upstreamCommittedFormat = null;\n    if (resetUpstreamFormat) {\n      upstreamFormat = null;\n      upstreamFormatRequired = true;\n    }\n  }\n\n  /**\n   * Returns the current absolute write index.\n   */\n  public int getWriteIndex() {\n    return absoluteFirstIndex + length;\n  }\n\n  /**\n   * Discards samples from the write side of the queue.\n   *\n   * @param discardFromIndex The absolute index of the first sample to be discarded.\n   * @return The reduced total number of bytes written after the samples have been discarded, or 0\n   *     if the queue is now empty.\n   */\n  public long discardUpstreamSamples(int discardFromIndex) {\n    int discardCount = getWriteIndex() - discardFromIndex;\n    Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));\n    length -= discardCount;\n    largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));\n    isLastSampleQueued = discardCount == 0 && isLastSampleQueued;\n    if (length == 0) {\n      return 0;\n    } else {\n      int relativeLastWriteIndex = getRelativeIndex(length - 1);\n      return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];\n    }\n  }\n\n  public void sourceId(int sourceId) {\n    upstreamSourceId = sourceId;\n  }\n\n  // Called by the consuming thread.\n\n  /**\n   * Throws an error that's preventing data from being read. Does nothing if no such error exists.\n   *\n   * @throws IOException The underlying error.\n   */\n  public void maybeThrowError() throws IOException {\n    // TODO: Avoid throwing if the DRM error is not preventing a read operation.\n    if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) {\n      throw Assertions.checkNotNull(currentDrmSession.getError());\n    }\n  }\n\n  /** Releases any owned {@link DrmSession} references. */\n  public void releaseDrmSessionReferences() {\n    if (currentDrmSession != null) {\n      currentDrmSession.release();\n      currentDrmSession = null;\n      // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData\n      // != null implies currentSession != null\n      downstreamFormat = null;\n    }\n  }\n\n  /** Returns the current absolute start index. */\n  public int getFirstIndex() {\n    return absoluteFirstIndex;\n  }\n\n  /**\n   * Returns the current absolute read index.\n   */\n  public int getReadIndex() {\n    return absoluteFirstIndex + readPosition;\n  }\n\n  /**\n   * Peeks the source id of the next sample to be read, or the current upstream source id if the\n   * queue is empty or if the read position is at the end of the queue.\n   *\n   * @return The source id.\n   */\n  public synchronized int peekSourceId() {\n    int relativeReadIndex = getRelativeIndex(readPosition);\n    return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;\n  }\n\n  /**\n   * Returns the upstream {@link Format} in which samples are being queued.\n   */\n  public synchronized Format getUpstreamFormat() {\n    return upstreamFormatRequired ? null : upstreamFormat;\n  }\n\n  /**\n   * Returns the largest sample timestamp that has been queued since the last call to\n   * {@link #reset(boolean)}.\n   * <p>\n   * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not\n   * considered as having been queued. Samples that were dequeued from the front of the queue are\n   * considered as having been queued.\n   *\n   * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no\n   *     samples have been queued.\n   */\n  public synchronized long getLargestQueuedTimestampUs() {\n    return largestQueuedTimestampUs;\n  }\n\n  /**\n   * Returns whether the last sample of the stream has knowingly been queued. A return value of\n   * {@code false} means that the last sample had not been queued or that it's unknown whether the\n   * last sample has been queued.\n   *\n   * <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not\n   * considered as having been queued. Samples that were dequeued from the front of the queue are\n   * considered as having been queued.\n   */\n  public synchronized boolean isLastSampleQueued() {\n    return isLastSampleQueued;\n  }\n\n  /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */\n  public synchronized long getFirstTimestampUs() {\n    return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];\n  }\n\n  /**\n   * Rewinds the read position to the first sample retained in the queue.\n   */\n  public synchronized void rewind() {\n    readPosition = 0;\n  }\n\n  /**\n   * Returns whether there is data available for reading.\n   *\n   * <p>Note: If the stream has ended then a buffer with the end of stream flag can always be read\n   * from {@link #read}. Hence an ended stream is always ready.\n   *\n   * @param loadingFinished Whether no more samples will be written to the sample queue. When true,\n   *     this method returns true if the sample queue is empty, because an empty sample queue means\n   *     the end of stream has been reached. When false, this method returns false if the sample\n   *     queue is empty.\n   */\n  public boolean isReady(boolean loadingFinished) {\n    if (!hasNextSample()) {\n      return loadingFinished\n          || isLastSampleQueued\n          || (upstreamFormat != null && upstreamFormat != downstreamFormat);\n    }\n    int relativeReadIndex = getRelativeIndex(readPosition);\n    if (formats[relativeReadIndex] != downstreamFormat) {\n      // A format can be read.\n      return true;\n    }\n    return mayReadSample(relativeReadIndex);\n  }\n\n  /**\n   * Attempts to read from the queue.\n   *\n   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.\n   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the\n   *     end of the stream. If a sample is read then the buffer is populated with information about\n   *     the sample, but not its data. The size and absolute position of the data in the rolling\n   *     buffer is stored in {@code extrasHolder}, along with an encryption id if present and the\n   *     absolute position of the first byte that may still be required after the current sample has\n   *     been read. If a {@link DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only\n   *     the buffer flags may be populated by this method and the read position of the queue will\n   *     not change. May be null if the caller requires that the format of the stream be read even\n   *     if it's not changing.\n   * @param formatRequired Whether the caller requires that the format of the stream be read even if\n   *     it's not changing. A sample will never be read if set to true, however it is still possible\n   *     for the end of stream or nothing to be read.\n   * @param loadingFinished True if an empty queue should be considered the end of the stream.\n   * @param extrasHolder The holder into which extra sample information should be written.\n   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or\n   *     {@link C#RESULT_BUFFER_READ}.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  public synchronized int read(\n      FormatHolder formatHolder,\n      DecoderInputBuffer buffer,\n      boolean formatRequired,\n      boolean loadingFinished,\n      SampleExtrasHolder extrasHolder) {\n    if (!hasNextSample()) {\n      if (loadingFinished || isLastSampleQueued) {\n        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n        return C.RESULT_BUFFER_READ;\n      } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) {\n        onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder);\n        return C.RESULT_FORMAT_READ;\n      } else {\n        return C.RESULT_NOTHING_READ;\n      }\n    }\n\n    int relativeReadIndex = getRelativeIndex(readPosition);\n    if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {\n      onFormatResult(formats[relativeReadIndex], formatHolder);\n      return C.RESULT_FORMAT_READ;\n    }\n\n    if (!mayReadSample(relativeReadIndex)) {\n      return C.RESULT_NOTHING_READ;\n    }\n\n    buffer.setFlags(flags[relativeReadIndex]);\n    buffer.timeUs = timesUs[relativeReadIndex];\n    if (buffer.isFlagsOnly()) {\n      return C.RESULT_BUFFER_READ;\n    }\n\n    extrasHolder.size = sizes[relativeReadIndex];\n    extrasHolder.offset = offsets[relativeReadIndex];\n    extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];\n\n    readPosition++;\n    return C.RESULT_BUFFER_READ;\n  }\n\n  /**\n   * Attempts to advance the read position to the sample before or at the specified time.\n   *\n   * @param timeUs The time to advance to.\n   * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified\n   *     time, rather than to any sample before or at that time.\n   * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the\n   *     end of the queue, by advancing the read position to the last sample (or keyframe) in the\n   *     queue.\n   * @return The number of samples that were skipped if the operation was successful, which may be\n   *     equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A\n   *     successful advance is one in which the read position was unchanged or advanced, and is now\n   *     at a sample meeting the specified criteria.\n   */\n  public synchronized int advanceTo(long timeUs, boolean toKeyframe,\n      boolean allowTimeBeyondBuffer) {\n    int relativeReadIndex = getRelativeIndex(readPosition);\n    if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]\n        || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {\n      return SampleQueue.ADVANCE_FAILED;\n    }\n    int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);\n    if (offset == -1) {\n      return SampleQueue.ADVANCE_FAILED;\n    }\n    readPosition += offset;\n    return offset;\n  }\n\n  /**\n   * Advances the read position to the end of the queue.\n   *\n   * @return The number of samples that were skipped.\n   */\n  public synchronized int advanceToEnd() {\n    int skipCount = length - readPosition;\n    readPosition = length;\n    return skipCount;\n  }\n\n  /**\n   * Attempts to set the read position to the specified sample index.\n   *\n   * @param sampleIndex The sample index.\n   * @return Whether the read position was set successfully. False is returned if the specified\n   *     index is smaller than the index of the first sample in the queue, or larger than the index\n   *     of the next sample that will be written.\n   */\n  public synchronized boolean setReadPosition(int sampleIndex) {\n    if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) {\n      readPosition = sampleIndex - absoluteFirstIndex;\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * Discards up to but not including the sample immediately before or at the specified time.\n   *\n   * @param timeUs The time to discard up to.\n   * @param toKeyframe If true then discards samples up to the keyframe before or at the specified\n   *     time, rather than just any sample before or at that time.\n   * @param stopAtReadPosition If true then samples are only discarded if they're before the read\n   *     position. If false then samples at and beyond the read position may be discarded, in which\n   *     case the read position is advanced to the first remaining sample.\n   * @return The corresponding offset up to which data should be discarded, or\n   *     {@link C#POSITION_UNSET} if no discarding of data is necessary.\n   */\n  public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {\n    if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {\n      return C.POSITION_UNSET;\n    }\n    int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;\n    int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);\n    if (discardCount == -1) {\n      return C.POSITION_UNSET;\n    }\n    return discardSamples(discardCount);\n  }\n\n  /**\n   * Discards samples up to but not including the read position.\n   *\n   * @return The corresponding offset up to which data should be discarded, or\n   *     {@link C#POSITION_UNSET} if no discarding of data is necessary.\n   */\n  public synchronized long discardToRead() {\n    if (readPosition == 0) {\n      return C.POSITION_UNSET;\n    }\n    return discardSamples(readPosition);\n  }\n\n  /**\n   * Discards all samples in the queue. The read position is also advanced.\n   *\n   * @return The corresponding offset up to which data should be discarded, or\n   *     {@link C#POSITION_UNSET} if no discarding of data is necessary.\n   */\n  public synchronized long discardToEnd() {\n    if (length == 0) {\n      return C.POSITION_UNSET;\n    }\n    return discardSamples(length);\n  }\n\n  // Called by the loading thread.\n\n  public synchronized boolean format(Format format) {\n    if (format == null) {\n      upstreamFormatRequired = true;\n      return false;\n    }\n    upstreamFormatRequired = false;\n    if (Util.areEqual(format, upstreamFormat)) {\n      // The format is unchanged. If format and upstreamFormat are different objects, we keep the\n      // current upstreamFormat so we can detect format changes in read() using cheap referential\n      // equality.\n      return false;\n    } else if (Util.areEqual(format, upstreamCommittedFormat)) {\n      // The format has changed back to the format of the last committed sample. If they are\n      // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat so\n      // we can detect format changes in read() using cheap referential equality.\n      upstreamFormat = upstreamCommittedFormat;\n      return true;\n    } else {\n      upstreamFormat = format;\n      return true;\n    }\n  }\n\n  public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,\n      int size, CryptoData cryptoData) {\n    if (upstreamKeyframeRequired) {\n      if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {\n        return;\n      }\n      upstreamKeyframeRequired = false;\n    }\n    Assertions.checkState(!upstreamFormatRequired);\n\n    isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;\n    largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);\n\n    int relativeEndIndex = getRelativeIndex(length);\n    timesUs[relativeEndIndex] = timeUs;\n    offsets[relativeEndIndex] = offset;\n    sizes[relativeEndIndex] = size;\n    flags[relativeEndIndex] = sampleFlags;\n    cryptoDatas[relativeEndIndex] = cryptoData;\n    formats[relativeEndIndex] = upstreamFormat;\n    sourceIds[relativeEndIndex] = upstreamSourceId;\n    upstreamCommittedFormat = upstreamFormat;\n\n    length++;\n    if (length == capacity) {\n      // Increase the capacity.\n      int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;\n      int[] newSourceIds = new int[newCapacity];\n      long[] newOffsets = new long[newCapacity];\n      long[] newTimesUs = new long[newCapacity];\n      int[] newFlags = new int[newCapacity];\n      int[] newSizes = new int[newCapacity];\n      CryptoData[] newCryptoDatas = new CryptoData[newCapacity];\n      Format[] newFormats = new Format[newCapacity];\n      int beforeWrap = capacity - relativeFirstIndex;\n      System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);\n      System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);\n      System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);\n      System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);\n      System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);\n      System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap);\n      System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);\n      int afterWrap = relativeFirstIndex;\n      System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);\n      System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);\n      System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);\n      System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);\n      System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);\n      System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);\n      System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);\n      offsets = newOffsets;\n      timesUs = newTimesUs;\n      flags = newFlags;\n      sizes = newSizes;\n      cryptoDatas = newCryptoDatas;\n      formats = newFormats;\n      sourceIds = newSourceIds;\n      relativeFirstIndex = 0;\n      length = capacity;\n      capacity = newCapacity;\n    }\n  }\n\n  /**\n   * Attempts to discard samples from the end of the queue to allow samples starting from the\n   * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.\n   *\n   * @param timeUs The timestamp at which the splice occurs.\n   * @return Whether the splice was successful.\n   */\n  public synchronized boolean attemptSplice(long timeUs) {\n    if (length == 0) {\n      return timeUs > largestDiscardedTimestampUs;\n    }\n    long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs,\n        getLargestTimestamp(readPosition));\n    if (largestReadTimestampUs >= timeUs) {\n      return false;\n    }\n    int retainCount = length;\n    int relativeSampleIndex = getRelativeIndex(length - 1);\n    while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {\n      retainCount--;\n      relativeSampleIndex--;\n      if (relativeSampleIndex == -1) {\n        relativeSampleIndex = capacity - 1;\n      }\n    }\n    discardUpstreamSamples(absoluteFirstIndex + retainCount);\n    return true;\n  }\n\n  // Internal methods.\n\n  private boolean hasNextSample() {\n    return readPosition != length;\n  }\n\n  /**\n   * Sets the downstream format, performs DRM resource management, and populates the {@code\n   * outputFormatHolder}.\n   *\n   * @param newFormat The new downstream format.\n   * @param outputFormatHolder The output {@link FormatHolder}.\n   */\n  private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) {\n    outputFormatHolder.format = newFormat;\n    boolean isFirstFormat = downstreamFormat == null;\n    DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData;\n    downstreamFormat = newFormat;\n    if (drmSessionManager == DrmSessionManager.DUMMY) {\n      // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that\n      // the media source creation has not yet been migrated and the renderer can acquire the\n      // session for the read DRM init data.\n      // TODO: Remove once renderers are migrated [Internal ref: b/122519809].\n      return;\n    }\n    DrmInitData newDrmInitData = newFormat.drmInitData;\n    outputFormatHolder.includesDrmSession = true;\n    outputFormatHolder.drmSession = currentDrmSession;\n    if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) {\n      // Nothing to do.\n      return;\n    }\n    // Ensure we acquire the new session before releasing the previous one in case the same session\n    // is being used for both DrmInitData.\n    DrmSession<?> previousSession = currentDrmSession;\n    Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());\n    currentDrmSession =\n        newDrmInitData != null\n            ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData)\n            : drmSessionManager.acquirePlaceholderSession(\n                playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));\n    outputFormatHolder.drmSession = currentDrmSession;\n\n    if (previousSession != null) {\n      previousSession.release();\n    }\n  }\n\n  /**\n   * Returns whether it's possible to read the next sample.\n   *\n   * @param relativeReadIndex The relative read index of the next sample.\n   * @return Whether it's possible to read the next sample.\n   */\n  private boolean mayReadSample(int relativeReadIndex) {\n    if (drmSessionManager == DrmSessionManager.DUMMY) {\n      // TODO: Remove once renderers are migrated [Internal ref: b/122519809].\n      // For protected content it's likely that the DrmSessionManager is still being injected into\n      // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed.\n      return true;\n    }\n    return currentDrmSession == null\n        || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS\n        || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0\n            && currentDrmSession.playClearSamplesWithoutKeys());\n  }\n\n  /**\n   * Finds the sample in the specified range that's before or at the specified time. If {@code\n   * keyframe} is {@code true} then the sample is additionally required to be a keyframe.\n   *\n   * @param relativeStartIndex The relative index from which to start searching.\n   * @param length The length of the range being searched.\n   * @param timeUs The specified time.\n   * @param keyframe Whether only keyframes should be considered.\n   * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching\n   *     sample was found.\n   */\n  private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {\n    // This could be optimized to use a binary search, however in practice callers to this method\n    // normally pass times near to the start of the search region. Hence it's unclear whether\n    // switching to a binary search would yield any real benefit.\n    int sampleCountToTarget = -1;\n    int searchIndex = relativeStartIndex;\n    for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) {\n      if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {\n        // We've found a suitable sample.\n        sampleCountToTarget = i;\n      }\n      searchIndex++;\n      if (searchIndex == capacity) {\n        searchIndex = 0;\n      }\n    }\n    return sampleCountToTarget;\n  }\n\n  /**\n   * Discards the specified number of samples.\n   *\n   * @param discardCount The number of samples to discard.\n   * @return The corresponding offset up to which data should be discarded.\n   */\n  private long discardSamples(int discardCount) {\n    largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs,\n        getLargestTimestamp(discardCount));\n    length -= discardCount;\n    absoluteFirstIndex += discardCount;\n    relativeFirstIndex += discardCount;\n    if (relativeFirstIndex >= capacity) {\n      relativeFirstIndex -= capacity;\n    }\n    readPosition -= discardCount;\n    if (readPosition < 0) {\n      readPosition = 0;\n    }\n    if (length == 0) {\n      int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;\n      return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];\n    } else {\n      return offsets[relativeFirstIndex];\n    }\n  }\n\n  /**\n   * Finds the largest timestamp of any sample from the start of the queue up to the specified\n   * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of\n   * the keyframe itself, and of subsequent frames.\n   *\n   * @param length The length of the range being searched.\n   * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}.\n   */\n  private long getLargestTimestamp(int length) {\n    if (length == 0) {\n      return Long.MIN_VALUE;\n    }\n    long largestTimestampUs = Long.MIN_VALUE;\n    int relativeSampleIndex = getRelativeIndex(length - 1);\n    for (int i = 0; i < length; i++) {\n      largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]);\n      if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {\n        break;\n      }\n      relativeSampleIndex--;\n      if (relativeSampleIndex == -1) {\n        relativeSampleIndex = capacity - 1;\n      }\n    }\n    return largestTimestampUs;\n  }\n\n   /**\n    * Returns the relative index for a given offset from the start of the queue.\n    *\n    * @param offset The offset, which must be in the range [0, length].\n    */\n  private int getRelativeIndex(int offset) {\n    int relativeIndex = relativeFirstIndex + offset;\n    return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder;\nimport com.google.android.exoplayer2.upstream.Allocation;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/** A queue of media samples. */\npublic class SampleQueue implements TrackOutput {\n\n  /**\n   * A listener for changes to the upstream format.\n   */\n  public interface UpstreamFormatChangedListener {\n\n    /**\n     * Called on the loading thread when an upstream format change occurs.\n     *\n     * @param format The new upstream format.\n     */\n    void onUpstreamFormatChanged(Format format);\n\n  }\n\n  public static final int ADVANCE_FAILED = -1;\n\n  private static final int INITIAL_SCRATCH_SIZE = 32;\n\n  private final Allocator allocator;\n  private final int allocationLength;\n  private final SampleMetadataQueue metadataQueue;\n  private final SampleExtrasHolder extrasHolder;\n  private final ParsableByteArray scratch;\n\n  // References into the linked list of allocations.\n  private AllocationNode firstAllocationNode;\n  private AllocationNode readAllocationNode;\n  private AllocationNode writeAllocationNode;\n\n  // Accessed only by the loading thread (or the consuming thread when there is no loading thread).\n  private boolean pendingFormatAdjustment;\n  private Format lastUnadjustedFormat;\n  private long sampleOffsetUs;\n  private long totalBytesWritten;\n  private boolean pendingSplice;\n  private UpstreamFormatChangedListener upstreamFormatChangeListener;\n\n  /**\n   * Creates a sample queue.\n   *\n   * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.\n   * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}\n   *     from. The created instance does not take ownership of this {@link DrmSessionManager}.\n   */\n  public SampleQueue(Allocator allocator, DrmSessionManager<?> drmSessionManager) {\n    this.allocator = allocator;\n    allocationLength = allocator.getIndividualAllocationLength();\n    metadataQueue = new SampleMetadataQueue(drmSessionManager);\n    extrasHolder = new SampleExtrasHolder();\n    scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);\n    firstAllocationNode = new AllocationNode(0, allocationLength);\n    readAllocationNode = firstAllocationNode;\n    writeAllocationNode = firstAllocationNode;\n  }\n\n  // Called by the consuming thread, but only when there is no loading thread.\n\n  /**\n   * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}.\n   */\n  public void reset() {\n    reset(false);\n  }\n\n  /**\n   * Resets the output.\n   *\n   * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,\n   *     samples queued after the reset (and before a subsequent call to {@link #format(Format)})\n   *     are assumed to have the current upstream format. If set to true, {@link #format(Format)}\n   *     must be called after the reset before any more samples can be queued.\n   */\n  public void reset(boolean resetUpstreamFormat) {\n    metadataQueue.reset(resetUpstreamFormat);\n    clearAllocationNodes(firstAllocationNode);\n    firstAllocationNode = new AllocationNode(0, allocationLength);\n    readAllocationNode = firstAllocationNode;\n    writeAllocationNode = firstAllocationNode;\n    totalBytesWritten = 0;\n    allocator.trim();\n  }\n\n  /**\n   * Sets a source identifier for subsequent samples.\n   *\n   * @param sourceId The source identifier.\n   */\n  public void sourceId(int sourceId) {\n    metadataQueue.sourceId(sourceId);\n  }\n\n  /**\n   * Indicates samples that are subsequently queued should be spliced into those already queued.\n   */\n  public void splice() {\n    pendingSplice = true;\n  }\n\n  /**\n   * Returns the current absolute write index.\n   */\n  public int getWriteIndex() {\n    return metadataQueue.getWriteIndex();\n  }\n\n  /**\n   * Discards samples from the write side of the queue.\n   *\n   * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the\n   *     range [{@link #getReadIndex()}, {@link #getWriteIndex()}].\n   */\n  public void discardUpstreamSamples(int discardFromIndex) {\n    totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex);\n    if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) {\n      clearAllocationNodes(firstAllocationNode);\n      firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength);\n      readAllocationNode = firstAllocationNode;\n      writeAllocationNode = firstAllocationNode;\n    } else {\n      // Find the last node containing at least 1 byte of data that we need to keep.\n      AllocationNode lastNodeToKeep = firstAllocationNode;\n      while (totalBytesWritten > lastNodeToKeep.endPosition) {\n        lastNodeToKeep = lastNodeToKeep.next;\n      }\n      // Discard all subsequent nodes.\n      AllocationNode firstNodeToDiscard = lastNodeToKeep.next;\n      clearAllocationNodes(firstNodeToDiscard);\n      // Reset the successor of the last node to be an uninitialized node.\n      lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength);\n      // Update writeAllocationNode and readAllocationNode as necessary.\n      writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next\n          : lastNodeToKeep;\n      if (readAllocationNode == firstNodeToDiscard) {\n        readAllocationNode = lastNodeToKeep.next;\n      }\n    }\n  }\n\n  // Called by the consuming thread.\n\n  /**\n   * Throws an error that's preventing data from being read. Does nothing if no such error exists.\n   *\n   * @throws IOException The underlying error.\n   */\n  public void maybeThrowError() throws IOException {\n    metadataQueue.maybeThrowError();\n  }\n\n  /**\n   * Returns the absolute index of the first sample.\n   */\n  public int getFirstIndex() {\n    return metadataQueue.getFirstIndex();\n  }\n\n  /**\n   * Returns the current absolute read index.\n   */\n  public int getReadIndex() {\n    return metadataQueue.getReadIndex();\n  }\n\n  /**\n   * Peeks the source id of the next sample to be read, or the current upstream source id if the\n   * queue is empty or if the read position is at the end of the queue.\n   *\n   * @return The source id.\n   */\n  public int peekSourceId() {\n    return metadataQueue.peekSourceId();\n  }\n\n  /**\n   * Returns the upstream {@link Format} in which samples are being queued.\n   */\n  public Format getUpstreamFormat() {\n    return metadataQueue.getUpstreamFormat();\n  }\n\n  /**\n   * Returns the largest sample timestamp that has been queued since the last {@link #reset}.\n   * <p>\n   * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not\n   * considered as having been queued. Samples that were dequeued from the front of the queue are\n   * considered as having been queued.\n   *\n   * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no\n   *     samples have been queued.\n   */\n  public long getLargestQueuedTimestampUs() {\n    return metadataQueue.getLargestQueuedTimestampUs();\n  }\n\n  /**\n   * Returns whether the last sample of the stream has knowingly been queued. A return value of\n   * {@code false} means that the last sample had not been queued or that it's unknown whether the\n   * last sample has been queued.\n   */\n  public boolean isLastSampleQueued() {\n    return metadataQueue.isLastSampleQueued();\n  }\n\n  /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */\n  public long getFirstTimestampUs() {\n    return metadataQueue.getFirstTimestampUs();\n  }\n\n  /**\n   * Rewinds the read position to the first sample in the queue.\n   */\n  public void rewind() {\n    metadataQueue.rewind();\n    readAllocationNode = firstAllocationNode;\n  }\n\n  /**\n   * Discards up to but not including the sample immediately before or at the specified time.\n   *\n   * @param timeUs The time to discard to.\n   * @param toKeyframe If true then discards samples up to the keyframe before or at the specified\n   *     time, rather than any sample before or at that time.\n   * @param stopAtReadPosition If true then samples are only discarded if they're before the\n   *     read position. If false then samples at and beyond the read position may be discarded, in\n   *     which case the read position is advanced to the first remaining sample.\n   */\n  public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {\n    discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition));\n  }\n\n  /**\n   * Discards up to but not including the read position.\n   */\n  public void discardToRead() {\n    discardDownstreamTo(metadataQueue.discardToRead());\n  }\n\n  /** Calls {@link #discardToEnd()} and releases any owned {@link DrmSession} references. */\n  public void preRelease() {\n    discardToEnd();\n    metadataQueue.releaseDrmSessionReferences();\n  }\n\n  /** Calls {@link #reset()} and releases any owned {@link DrmSession} references. */\n  public void release() {\n    reset();\n    metadataQueue.releaseDrmSessionReferences();\n  }\n\n  /**\n   * Discards to the end of the queue. The read position is also advanced.\n   */\n  public void discardToEnd() {\n    discardDownstreamTo(metadataQueue.discardToEnd());\n  }\n\n  /**\n   * Advances the read position to the end of the queue.\n   *\n   * @return The number of samples that were skipped.\n   */\n  public int advanceToEnd() {\n    return metadataQueue.advanceToEnd();\n  }\n\n  /**\n   * Attempts to advance the read position to the sample before or at the specified time.\n   *\n   * @param timeUs The time to advance to.\n   * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified\n   *     time, rather than to any sample before or at that time.\n   * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the\n   *     end of the queue, by advancing the read position to the last sample (or keyframe).\n   * @return The number of samples that were skipped if the operation was successful, which may be\n   *     equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful\n   *     advance is one in which the read position was unchanged or advanced, and is now at a sample\n   *     meeting the specified criteria.\n   */\n  public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {\n    return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);\n  }\n\n  /**\n   * Attempts to set the read position to the specified sample index.\n   *\n   * @param sampleIndex The sample index.\n   * @return Whether the read position was set successfully. False is returned if the specified\n   *     index is smaller than the index of the first sample in the queue, or larger than the index\n   *     of the next sample that will be written.\n   */\n  public boolean setReadPosition(int sampleIndex) {\n    return metadataQueue.setReadPosition(sampleIndex);\n  }\n\n  /**\n   * Attempts to read from the queue.\n   *\n   * <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}\n   * through {@link FormatHolder#drmSession}, which is populated in two scenarios:\n   *\n   * <ul>\n   *   <li>The {@link Format} has a non-null {@link Format#drmInitData}.\n   *   <li>The {@link DrmSessionManager} provides placeholder sessions for this queue's track type.\n   *       See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}.\n   * </ul>\n   *\n   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.\n   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the\n   *     end of the stream. If the end of the stream has been reached, the {@link\n   *     C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link\n   *     DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only the buffer flags may be\n   *     populated by this method and the read position of the queue will not change.\n   * @param formatRequired Whether the caller requires that the format of the stream be read even if\n   *     it's not changing. A sample will never be read if set to true, however it is still possible\n   *     for the end of stream or nothing to be read.\n   * @param loadingFinished True if an empty queue should be considered the end of the stream.\n   * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will\n   *     be set if the buffer's timestamp is less than this value.\n   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or\n   *     {@link C#RESULT_BUFFER_READ}.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  public int read(\n      FormatHolder formatHolder,\n      DecoderInputBuffer buffer,\n      boolean formatRequired,\n      boolean loadingFinished,\n      long decodeOnlyUntilUs) {\n    int result =\n        metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, extrasHolder);\n    if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {\n      if (buffer.timeUs < decodeOnlyUntilUs) {\n        buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);\n      }\n      if (!buffer.isFlagsOnly()) {\n        readToBuffer(buffer, extrasHolder);\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Returns whether there is data available for reading.\n   *\n   * <p>Note: If the stream has ended then a buffer with the end of stream flag can always be read\n   * from {@link #read}. Hence an ended stream is always ready.\n   *\n   * @param loadingFinished Whether no more samples will be written to the sample queue. When true,\n   *     this method returns true if the sample queue is empty, because an empty sample queue means\n   *     the end of stream has been reached. When false, this method returns false if the sample\n   *     queue is empty.\n   */\n  public boolean isReady(boolean loadingFinished) {\n    return metadataQueue.isReady(loadingFinished);\n  }\n\n  /**\n   * Reads data from the rolling buffer to populate a decoder input buffer.\n   *\n   * @param buffer The buffer to populate.\n   * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.\n   */\n  private void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {\n    // Read encryption data if the sample is encrypted.\n    if (buffer.isEncrypted()) {\n      readEncryptionData(buffer, extrasHolder);\n    }\n    // Read sample data, extracting supplemental data into a separate buffer if needed.\n    if (buffer.hasSupplementalData()) {\n      // If there is supplemental data, the sample data is prefixed by its size.\n      scratch.reset(4);\n      readData(extrasHolder.offset, scratch.data, 4);\n      int sampleSize = scratch.readUnsignedIntToInt();\n      extrasHolder.offset += 4;\n      extrasHolder.size -= 4;\n\n      // Write the sample data.\n      buffer.ensureSpaceForWrite(sampleSize);\n      readData(extrasHolder.offset, buffer.data, sampleSize);\n      extrasHolder.offset += sampleSize;\n      extrasHolder.size -= sampleSize;\n\n      // Write the remaining data as supplemental data.\n      buffer.resetSupplementalData(extrasHolder.size);\n      readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size);\n    } else {\n      // Write the sample data.\n      buffer.ensureSpaceForWrite(extrasHolder.size);\n      readData(extrasHolder.offset, buffer.data, extrasHolder.size);\n    }\n  }\n\n  /**\n   * Reads encryption data for the current sample.\n   *\n   * <p>The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link\n   * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same\n   * value is added to {@link SampleExtrasHolder#offset}.\n   *\n   * @param buffer The buffer into which the encryption data should be written.\n   * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.\n   */\n  private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {\n    long offset = extrasHolder.offset;\n\n    // Read the signal byte.\n    scratch.reset(1);\n    readData(offset, scratch.data, 1);\n    offset++;\n    byte signalByte = scratch.data[0];\n    boolean subsampleEncryption = (signalByte & 0x80) != 0;\n    int ivSize = signalByte & 0x7F;\n\n    // Read the initialization vector.\n    if (buffer.cryptoInfo.iv == null) {\n      buffer.cryptoInfo.iv = new byte[16];\n    }\n    readData(offset, buffer.cryptoInfo.iv, ivSize);\n    offset += ivSize;\n\n    // Read the subsample count, if present.\n    int subsampleCount;\n    if (subsampleEncryption) {\n      scratch.reset(2);\n      readData(offset, scratch.data, 2);\n      offset += 2;\n      subsampleCount = scratch.readUnsignedShort();\n    } else {\n      subsampleCount = 1;\n    }\n\n    // Write the clear and encrypted subsample sizes.\n    int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;\n    if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {\n      clearDataSizes = new int[subsampleCount];\n    }\n    int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;\n    if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {\n      encryptedDataSizes = new int[subsampleCount];\n    }\n    if (subsampleEncryption) {\n      int subsampleDataLength = 6 * subsampleCount;\n      scratch.reset(subsampleDataLength);\n      readData(offset, scratch.data, subsampleDataLength);\n      offset += subsampleDataLength;\n      scratch.setPosition(0);\n      for (int i = 0; i < subsampleCount; i++) {\n        clearDataSizes[i] = scratch.readUnsignedShort();\n        encryptedDataSizes[i] = scratch.readUnsignedIntToInt();\n      }\n    } else {\n      clearDataSizes[0] = 0;\n      encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);\n    }\n\n    // Populate the cryptoInfo.\n    CryptoData cryptoData = extrasHolder.cryptoData;\n    buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,\n        cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode,\n        cryptoData.encryptedBlocks, cryptoData.clearBlocks);\n\n    // Adjust the offset and size to take into account the bytes read.\n    int bytesRead = (int) (offset - extrasHolder.offset);\n    extrasHolder.offset += bytesRead;\n    extrasHolder.size -= bytesRead;\n  }\n\n  /**\n   * Reads data from the front of the rolling buffer.\n   *\n   * @param absolutePosition The absolute position from which data should be read.\n   * @param target The buffer into which data should be written.\n   * @param length The number of bytes to read.\n   */\n  private void readData(long absolutePosition, ByteBuffer target, int length) {\n    advanceReadTo(absolutePosition);\n    int remaining = length;\n    while (remaining > 0) {\n      int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));\n      Allocation allocation = readAllocationNode.allocation;\n      target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);\n      remaining -= toCopy;\n      absolutePosition += toCopy;\n      if (absolutePosition == readAllocationNode.endPosition) {\n        readAllocationNode = readAllocationNode.next;\n      }\n    }\n  }\n\n  /**\n   * Reads data from the front of the rolling buffer.\n   *\n   * @param absolutePosition The absolute position from which data should be read.\n   * @param target The array into which data should be written.\n   * @param length The number of bytes to read.\n   */\n  private void readData(long absolutePosition, byte[] target, int length) {\n    advanceReadTo(absolutePosition);\n    int remaining = length;\n    while (remaining > 0) {\n      int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));\n      Allocation allocation = readAllocationNode.allocation;\n      System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition),\n          target, length - remaining, toCopy);\n      remaining -= toCopy;\n      absolutePosition += toCopy;\n      if (absolutePosition == readAllocationNode.endPosition) {\n        readAllocationNode = readAllocationNode.next;\n      }\n    }\n  }\n\n  /**\n   * Advances {@link #readAllocationNode} to the specified absolute position.\n   *\n   * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.\n   */\n  private void advanceReadTo(long absolutePosition) {\n    while (absolutePosition >= readAllocationNode.endPosition) {\n      readAllocationNode = readAllocationNode.next;\n    }\n  }\n\n  /**\n   * Advances {@link #firstAllocationNode} to the specified absolute position.\n   * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind\n   * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their\n   * underlying allocations are returned to the allocator.\n   *\n   * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced.\n   *     May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op.\n   */\n  private void discardDownstreamTo(long absolutePosition) {\n    if (absolutePosition == C.POSITION_UNSET) {\n      return;\n    }\n    while (absolutePosition >= firstAllocationNode.endPosition) {\n      allocator.release(firstAllocationNode.allocation);\n      firstAllocationNode = firstAllocationNode.clear();\n    }\n    // If we discarded the node referenced by readAllocationNode then we need to advance it to the\n    // first remaining node.\n    if (readAllocationNode.startPosition < firstAllocationNode.startPosition) {\n      readAllocationNode = firstAllocationNode;\n    }\n  }\n\n  // Called by the loading thread.\n\n  /**\n   * Sets a listener to be notified of changes to the upstream format.\n   *\n   * @param listener The listener.\n   */\n  public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {\n    upstreamFormatChangeListener = listener;\n  }\n\n  /**\n   * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples\n   * that are subsequently queued.\n   *\n   * @param sampleOffsetUs The timestamp offset in microseconds.\n   */\n  public void setSampleOffsetUs(long sampleOffsetUs) {\n    if (this.sampleOffsetUs != sampleOffsetUs) {\n      this.sampleOffsetUs = sampleOffsetUs;\n      pendingFormatAdjustment = true;\n    }\n  }\n\n  @Override\n  public void format(Format format) {\n    Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);\n    boolean formatChanged = metadataQueue.format(adjustedFormat);\n    lastUnadjustedFormat = format;\n    pendingFormatAdjustment = false;\n    if (upstreamFormatChangeListener != null && formatChanged) {\n      upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);\n    }\n  }\n\n  @Override\n  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    length = preAppend(length);\n    int bytesAppended = input.read(writeAllocationNode.allocation.data,\n        writeAllocationNode.translateOffset(totalBytesWritten), length);\n    if (bytesAppended == C.RESULT_END_OF_INPUT) {\n      if (allowEndOfInput) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      throw new EOFException();\n    }\n    postAppend(bytesAppended);\n    return bytesAppended;\n  }\n\n  @Override\n  public void sampleData(ParsableByteArray buffer, int length) {\n    while (length > 0) {\n      int bytesAppended = preAppend(length);\n      buffer.readBytes(writeAllocationNode.allocation.data,\n          writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended);\n      length -= bytesAppended;\n      postAppend(bytesAppended);\n    }\n  }\n\n  @Override\n  public void sampleMetadata(\n      long timeUs,\n      @C.BufferFlags int flags,\n      int size,\n      int offset,\n      @Nullable CryptoData cryptoData) {\n    if (pendingFormatAdjustment) {\n      format(lastUnadjustedFormat);\n    }\n    timeUs += sampleOffsetUs;\n    if (pendingSplice) {\n      if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {\n        return;\n      }\n      pendingSplice = false;\n    }\n    long absoluteOffset = totalBytesWritten - size - offset;\n    metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);\n  }\n\n  // Private methods.\n\n  /**\n   * Clears allocation nodes starting from {@code fromNode}.\n   *\n   * @param fromNode The node from which to clear.\n   */\n  private void clearAllocationNodes(AllocationNode fromNode) {\n    if (!fromNode.wasInitialized) {\n      return;\n    }\n    // Bulk release allocations for performance (it's significantly faster when using\n    // DefaultAllocator because the allocator's lock only needs to be acquired and released once)\n    // [Internal: See b/29542039].\n    int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0)\n        + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength);\n    Allocation[] allocationsToRelease = new Allocation[allocationCount];\n    AllocationNode currentNode = fromNode;\n    for (int i = 0; i < allocationsToRelease.length; i++) {\n      allocationsToRelease[i] = currentNode.allocation;\n      currentNode = currentNode.clear();\n    }\n    allocator.release(allocationsToRelease);\n  }\n\n  /**\n   * Called before writing sample data to {@link #writeAllocationNode}. May cause\n   * {@link #writeAllocationNode} to be initialized.\n   *\n   * @param length The number of bytes that the caller wishes to write.\n   * @return The number of bytes that the caller is permitted to write, which may be less than\n   *     {@code length}.\n   */\n  private int preAppend(int length) {\n    if (!writeAllocationNode.wasInitialized) {\n      writeAllocationNode.initialize(allocator.allocate(),\n          new AllocationNode(writeAllocationNode.endPosition, allocationLength));\n    }\n    return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten));\n  }\n\n  /**\n   * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced.\n   *\n   * @param length The number of bytes that were written.\n   */\n  private void postAppend(int length) {\n    totalBytesWritten += length;\n    if (totalBytesWritten == writeAllocationNode.endPosition) {\n      writeAllocationNode = writeAllocationNode.next;\n    }\n  }\n\n  /**\n   * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.\n   *\n   * @param format The {@link Format} to adjust.\n   * @param sampleOffsetUs The offset to apply.\n   * @return The adjusted {@link Format}.\n   */\n  private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {\n    if (format == null) {\n      return null;\n    }\n    if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {\n      format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);\n    }\n    return format;\n  }\n\n  /** A node in a linked list of {@link Allocation}s held by the output. */\n  private static final class AllocationNode {\n\n    /**\n     * The absolute position of the start of the data (inclusive).\n     */\n    public final long startPosition;\n    /**\n     * The absolute position of the end of the data (exclusive).\n     */\n    public final long endPosition;\n    /**\n     * Whether the node has been initialized. Remains true after {@link #clear()}.\n     */\n    public boolean wasInitialized;\n    /**\n     * The {@link Allocation}, or {@code null} if the node is not initialized.\n     */\n    @Nullable public Allocation allocation;\n    /**\n     * The next {@link AllocationNode} in the list, or {@code null} if the node has not been\n     * initialized. Remains set after {@link #clear()}.\n     */\n    @Nullable public AllocationNode next;\n\n    /**\n     * @param startPosition See {@link #startPosition}.\n     * @param allocationLength The length of the {@link Allocation} with which this node will be\n     *     initialized.\n     */\n    public AllocationNode(long startPosition, int allocationLength) {\n      this.startPosition = startPosition;\n      this.endPosition = startPosition + allocationLength;\n    }\n\n    /**\n     * Initializes the node.\n     *\n     * @param allocation The node's {@link Allocation}.\n     * @param next The next {@link AllocationNode}.\n     */\n    public void initialize(Allocation allocation, AllocationNode next) {\n      this.allocation = allocation;\n      this.next = next;\n      wasInitialized = true;\n    }\n\n    /**\n     * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to\n     * the specified absolute position.\n     *\n     * @param absolutePosition The absolute position.\n     * @return The corresponding offset into the allocation's data.\n     */\n    public int translateOffset(long absolutePosition) {\n      return (int) (absolutePosition - startPosition) + allocation.offset;\n    }\n\n    /**\n     * Clears {@link #allocation} and {@link #next}.\n     *\n     * @return The cleared next {@link AllocationNode}.\n     */\n    public AllocationNode clear() {\n      allocation = null;\n      AllocationNode temp = next;\n      next = null;\n      return temp;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SampleStream.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport java.io.IOException;\n\n/**\n * A stream of media samples (and associated format information).\n */\npublic interface SampleStream {\n\n  /**\n   * Returns whether data is available to be read.\n   * <p>\n   * Note: If the stream has ended then a buffer with the end of stream flag can always be read from\n   * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always\n   * ready.\n   *\n   * @return Whether data is available to be read.\n   */\n  boolean isReady();\n\n  /**\n   * Throws an error that's preventing data from being read. Does nothing if no such error exists.\n   *\n   * @throws IOException The underlying error.\n   */\n  void maybeThrowError() throws IOException;\n\n  /**\n   * Attempts to read from the stream.\n   *\n   * <p>If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code\n   * buffer} and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then {@link\n   * C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if {@code\n   * formatRequired} is set then {@code formatHolder} is populated and {@link C#RESULT_FORMAT_READ}\n   * is returned. Else {@code buffer} is populated and {@link C#RESULT_BUFFER_READ} is returned.\n   *\n   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.\n   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the\n   *     end of the stream. If the end of the stream has been reached, the {@link\n   *     C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link\n   *     DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, then no {@link\n   *     DecoderInputBuffer#data} will be read and the read position of the stream will not change,\n   *     but the flags of the buffer will be populated.\n   * @param formatRequired Whether the caller requires that the format of the stream be read even if\n   *     it's not changing. A sample will never be read if set to true, however it is still possible\n   *     for the end of stream or nothing to be read.\n   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or\n   *     {@link C#RESULT_BUFFER_READ}.\n   */\n  int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired);\n\n  /**\n   * Attempts to skip to the keyframe before the specified position, or to the end of the stream if\n   * {@code positionUs} is beyond it.\n   *\n   * @param positionUs The specified time.\n   * @return The number of samples that were skipped.\n   */\n  int skipData(long positionUs);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\n\n// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203].\n/**\n * A loader that can proceed in approximate synchronization with other loaders.\n */\npublic interface SequenceableLoader {\n\n  /**\n   * A callback to be notified of {@link SequenceableLoader} events.\n   */\n  interface Callback<T extends SequenceableLoader> {\n\n    /**\n     * Called by the loader to indicate that it wishes for its {@link #continueLoading(long)} method\n     * to be called when it can continue to load data. Called on the playback thread.\n     */\n    void onContinueLoadingRequested(T source);\n\n  }\n\n  /**\n   * Returns an estimate of the position up to which data is buffered.\n   *\n   * @return An estimate of the absolute position in microseconds up to which data is buffered, or\n   *     {@link C#TIME_END_OF_SOURCE} if the data is fully buffered.\n   */\n  long getBufferedPositionUs();\n\n  /**\n   * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.\n   */\n  long getNextLoadPositionUs();\n\n  /**\n   * Attempts to continue loading.\n   *\n   * @param positionUs The current playback position in microseconds. If playback of the period to\n   *     which this loader belongs has not yet started, the value will be the starting position\n   *     in the period minus the duration of any media in previous periods still to be played.\n   * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return\n   *     a different value than prior to the call. False otherwise.\n   */\n  boolean continueLoading(long positionUs);\n\n  /** Returns whether the loader is currently loading. */\n  boolean isLoading();\n\n  /**\n   * Re-evaluates the buffer given the playback position.\n   *\n   * <p>Re-evaluation may discard buffered media so that it can be re-buffered in a different\n   * quality.\n   *\n   * @param positionUs The current playback position in microseconds. If playback of this period has\n   *     not yet started, the value will be the starting position in this period minus the duration\n   *     of any media in previous periods still to be played.\n   */\n  void reevaluateBuffer(long positionUs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport com.google.android.exoplayer2.C;\nimport java.util.Arrays;\nimport java.util.Random;\n\n/**\n * Shuffled order of indices.\n *\n * <p>The shuffle order must be immutable to ensure thread safety.\n */\npublic interface ShuffleOrder {\n\n  /**\n   * The default {@link ShuffleOrder} implementation for random shuffle order.\n   */\n  class DefaultShuffleOrder implements ShuffleOrder {\n\n    private final Random random;\n    private final int[] shuffled;\n    private final int[] indexInShuffled;\n\n    /**\n     * Creates an instance with a specified length.\n     *\n     * @param length The length of the shuffle order.\n     */\n    public DefaultShuffleOrder(int length) {\n      this(length, new Random());\n    }\n\n    /**\n     * Creates an instance with a specified length and the specified random seed. Shuffle orders of\n     * the same length initialized with the same random seed are guaranteed to be equal.\n     *\n     * @param length The length of the shuffle order.\n     * @param randomSeed A random seed.\n     */\n    public DefaultShuffleOrder(int length, long randomSeed) {\n      this(length, new Random(randomSeed));\n    }\n\n    /**\n     * Creates an instance with a specified shuffle order and the specified random seed. The random\n     * seed is used for {@link #cloneAndInsert(int, int)} invocations.\n     *\n     * @param shuffledIndices The shuffled indices to use as order.\n     * @param randomSeed A random seed.\n     */\n    public DefaultShuffleOrder(int[] shuffledIndices, long randomSeed) {\n      this(Arrays.copyOf(shuffledIndices, shuffledIndices.length), new Random(randomSeed));\n    }\n\n    private DefaultShuffleOrder(int length, Random random) {\n      this(createShuffledList(length, random), random);\n    }\n\n    private DefaultShuffleOrder(int[] shuffled, Random random) {\n      this.shuffled = shuffled;\n      this.random = random;\n      this.indexInShuffled = new int[shuffled.length];\n      for (int i = 0; i < shuffled.length; i++) {\n        indexInShuffled[shuffled[i]] = i;\n      }\n    }\n\n    @Override\n    public int getLength() {\n      return shuffled.length;\n    }\n\n    @Override\n    public int getNextIndex(int index) {\n      int shuffledIndex = indexInShuffled[index];\n      return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getPreviousIndex(int index) {\n      int shuffledIndex = indexInShuffled[index];\n      return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getLastIndex() {\n      return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getFirstIndex() {\n      return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET;\n    }\n\n    @Override\n    public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {\n      int[] insertionPoints = new int[insertionCount];\n      int[] insertionValues = new int[insertionCount];\n      for (int i = 0; i < insertionCount; i++) {\n        insertionPoints[i] = random.nextInt(shuffled.length + 1);\n        int swapIndex = random.nextInt(i + 1);\n        insertionValues[i] = insertionValues[swapIndex];\n        insertionValues[swapIndex] = i + insertionIndex;\n      }\n      Arrays.sort(insertionPoints);\n      int[] newShuffled = new int[shuffled.length + insertionCount];\n      int indexInOldShuffled = 0;\n      int indexInInsertionList = 0;\n      for (int i = 0; i < shuffled.length + insertionCount; i++) {\n        if (indexInInsertionList < insertionCount\n            && indexInOldShuffled == insertionPoints[indexInInsertionList]) {\n          newShuffled[i] = insertionValues[indexInInsertionList++];\n        } else {\n          newShuffled[i] = shuffled[indexInOldShuffled++];\n          if (newShuffled[i] >= insertionIndex) {\n            newShuffled[i] += insertionCount;\n          }\n        }\n      }\n      return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));\n    }\n\n    @Override\n    public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {\n      int numberOfElementsToRemove = indexToExclusive - indexFrom;\n      int[] newShuffled = new int[shuffled.length - numberOfElementsToRemove];\n      int foundElementsCount = 0;\n      for (int i = 0; i < shuffled.length; i++) {\n        if (shuffled[i] >= indexFrom && shuffled[i] < indexToExclusive) {\n          foundElementsCount++;\n        } else {\n          newShuffled[i - foundElementsCount] =\n              shuffled[i] >= indexFrom ? shuffled[i] - numberOfElementsToRemove : shuffled[i];\n        }\n      }\n      return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));\n    }\n\n    @Override\n    public ShuffleOrder cloneAndClear() {\n      return new DefaultShuffleOrder(/* length= */ 0, new Random(random.nextLong()));\n    }\n\n    private static int[] createShuffledList(int length, Random random) {\n      int[] shuffled = new int[length];\n      for (int i = 0; i < length; i++) {\n        int swapIndex = random.nextInt(i + 1);\n        shuffled[i] = shuffled[swapIndex];\n        shuffled[swapIndex] = i;\n      }\n      return shuffled;\n    }\n\n  }\n\n  /**\n   * A {@link ShuffleOrder} implementation which does not shuffle.\n   */\n  final class UnshuffledShuffleOrder implements ShuffleOrder {\n\n    private final int length;\n\n    /**\n     * Creates an instance with a specified length.\n     *\n     * @param length The length of the shuffle order.\n     */\n    public UnshuffledShuffleOrder(int length) {\n      this.length = length;\n    }\n\n    @Override\n    public int getLength() {\n      return length;\n    }\n\n    @Override\n    public int getNextIndex(int index) {\n      return ++index < length ? index : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getPreviousIndex(int index) {\n      return --index >= 0 ? index : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getLastIndex() {\n      return length > 0 ? length - 1 : C.INDEX_UNSET;\n    }\n\n    @Override\n    public int getFirstIndex() {\n      return length > 0 ? 0 : C.INDEX_UNSET;\n    }\n\n    @Override\n    public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {\n      return new UnshuffledShuffleOrder(length + insertionCount);\n    }\n\n    @Override\n    public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {\n      return new UnshuffledShuffleOrder(length - indexToExclusive + indexFrom);\n    }\n\n    @Override\n    public ShuffleOrder cloneAndClear() {\n      return new UnshuffledShuffleOrder(/* length= */ 0);\n    }\n  }\n\n  /**\n   * Returns length of shuffle order.\n   */\n  int getLength();\n\n  /**\n   * Returns the next index in the shuffle order.\n   *\n   * @param index An index.\n   * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last\n   *     element.\n   */\n  int getNextIndex(int index);\n\n  /**\n   * Returns the previous index in the shuffle order.\n   *\n   * @param index An index.\n   * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first\n   *     element.\n   */\n  int getPreviousIndex(int index);\n\n  /**\n   * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is\n   * empty.\n   */\n  int getLastIndex();\n\n  /**\n   * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is\n   * empty.\n   */\n  int getFirstIndex();\n\n  /**\n   * Returns a copy of the shuffle order with newly inserted elements.\n   *\n   * @param insertionIndex The index in the unshuffled order at which elements are inserted.\n   * @param insertionCount The number of elements inserted at {@code insertionIndex}.\n   * @return A copy of this {@link ShuffleOrder} with newly inserted elements.\n   */\n  ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);\n\n  /**\n   * Returns a copy of the shuffle order with a range of elements removed.\n   *\n   * @param indexFrom The starting index in the unshuffled order of the range to remove.\n   * @param indexToExclusive The smallest index (must be greater or equal to {@code indexFrom}) that\n   *     will not be removed.\n   * @return A copy of this {@link ShuffleOrder} without the elements in the removed range.\n   */\n  ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive);\n\n  /** Returns a copy of the shuffle order with all elements removed. */\n  ShuffleOrder cloneAndClear();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** Media source with a single period consisting of silent raw audio of a given duration. */\npublic final class SilenceMediaSource extends BaseMediaSource {\n\n  private static final int SAMPLE_RATE_HZ = 44100;\n  @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT;\n  private static final int CHANNEL_COUNT = 2;\n  private static final Format FORMAT =\n      Format.createAudioSampleFormat(\n          /* id=*/ null,\n          MimeTypes.AUDIO_RAW,\n          /* codecs= */ null,\n          /* bitrate= */ Format.NO_VALUE,\n          /* maxInputSize= */ Format.NO_VALUE,\n          CHANNEL_COUNT,\n          SAMPLE_RATE_HZ,\n          ENCODING,\n          /* initializationData= */ null,\n          /* drmInitData= */ null,\n          /* selectionFlags= */ 0,\n          /* language= */ null);\n  private static final byte[] SILENCE_SAMPLE =\n      new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024];\n\n  private final long durationUs;\n\n  /**\n   * Creates a new media source providing silent audio of the given duration.\n   *\n   * @param durationUs The duration of silent audio to output, in microseconds.\n   */\n  public SilenceMediaSource(long durationUs) {\n    Assertions.checkArgument(durationUs >= 0);\n    this.durationUs = durationUs;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    refreshSourceInfo(\n        new SinglePeriodTimeline(\n            durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false));\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() {}\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    return new SilenceMediaPeriod(durationUs);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {}\n\n  @Override\n  protected void releaseSourceInternal() {}\n\n  private static final class SilenceMediaPeriod implements MediaPeriod {\n\n    private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT));\n\n    private final long durationUs;\n    private final ArrayList<SampleStream> sampleStreams;\n\n    public SilenceMediaPeriod(long durationUs) {\n      this.durationUs = durationUs;\n      sampleStreams = new ArrayList<>();\n    }\n\n    @Override\n    public void prepare(Callback callback, long positionUs) {\n      callback.onPrepared(/* mediaPeriod= */ this);\n    }\n\n    @Override\n    public void maybeThrowPrepareError() {}\n\n    @Override\n    public TrackGroupArray getTrackGroups() {\n      return TRACKS;\n    }\n\n    @Override\n    public long selectTracks(\n        @NullableType TrackSelection[] selections,\n        boolean[] mayRetainStreamFlags,\n        @NullableType SampleStream[] streams,\n        boolean[] streamResetFlags,\n        long positionUs) {\n      positionUs = constrainSeekPosition(positionUs);\n      for (int i = 0; i < selections.length; i++) {\n        if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n          sampleStreams.remove(streams[i]);\n          streams[i] = null;\n        }\n        if (streams[i] == null && selections[i] != null) {\n          SilenceSampleStream stream = new SilenceSampleStream(durationUs);\n          stream.seekTo(positionUs);\n          sampleStreams.add(stream);\n          streams[i] = stream;\n          streamResetFlags[i] = true;\n        }\n      }\n      return positionUs;\n    }\n\n    @Override\n    public void discardBuffer(long positionUs, boolean toKeyframe) {}\n\n    @Override\n    public long readDiscontinuity() {\n      return C.TIME_UNSET;\n    }\n\n    @Override\n    public long seekToUs(long positionUs) {\n      positionUs = constrainSeekPosition(positionUs);\n      for (int i = 0; i < sampleStreams.size(); i++) {\n        ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);\n      }\n      return positionUs;\n    }\n\n    @Override\n    public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n      return constrainSeekPosition(positionUs);\n    }\n\n    @Override\n    public long getBufferedPositionUs() {\n      return C.TIME_END_OF_SOURCE;\n    }\n\n    @Override\n    public long getNextLoadPositionUs() {\n      return C.TIME_END_OF_SOURCE;\n    }\n\n    @Override\n    public boolean continueLoading(long positionUs) {\n      return false;\n    }\n\n    @Override\n    public boolean isLoading() {\n      return false;\n    }\n\n    @Override\n    public void reevaluateBuffer(long positionUs) {}\n\n    private long constrainSeekPosition(long positionUs) {\n      return Util.constrainValue(positionUs, 0, durationUs);\n    }\n  }\n\n  private static final class SilenceSampleStream implements SampleStream {\n\n    private final long durationBytes;\n\n    private boolean sentFormat;\n    private long positionBytes;\n\n    public SilenceSampleStream(long durationUs) {\n      durationBytes = getAudioByteCount(durationUs);\n      seekTo(0);\n    }\n\n    public void seekTo(long positionUs) {\n      positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);\n    }\n\n    @Override\n    public boolean isReady() {\n      return true;\n    }\n\n    @Override\n    public void maybeThrowError() {}\n\n    @Override\n    public int readData(\n        FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {\n      if (!sentFormat || formatRequired) {\n        formatHolder.format = FORMAT;\n        sentFormat = true;\n        return C.RESULT_FORMAT_READ;\n      }\n\n      long bytesRemaining = durationBytes - positionBytes;\n      if (bytesRemaining == 0) {\n        buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n        return C.RESULT_BUFFER_READ;\n      }\n\n      int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining);\n      buffer.ensureSpaceForWrite(bytesToWrite);\n      buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite);\n      buffer.timeUs = getAudioPositionUs(positionBytes);\n      buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);\n      positionBytes += bytesToWrite;\n      return C.RESULT_BUFFER_READ;\n    }\n\n    @Override\n    public int skipData(long positionUs) {\n      long oldPositionBytes = positionBytes;\n      seekTo(positionUs);\n      return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length);\n    }\n  }\n\n  private static long getAudioByteCount(long durationUs) {\n    long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND;\n    return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount;\n  }\n\n  private static long getAudioPositionUs(long bytes) {\n    long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT);\n    return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * A {@link Timeline} consisting of a single period and static window.\n */\npublic final class SinglePeriodTimeline extends Timeline {\n\n  private static final Object UID = new Object();\n\n  private final long presentationStartTimeMs;\n  private final long windowStartTimeMs;\n  private final long periodDurationUs;\n  private final long windowDurationUs;\n  private final long windowPositionInPeriodUs;\n  private final long windowDefaultStartPositionUs;\n  private final boolean isSeekable;\n  private final boolean isDynamic;\n  private final boolean isLive;\n  @Nullable private final Object tag;\n  @Nullable private final Object manifest;\n\n  /**\n   * Creates a timeline containing a single period and a window that spans it.\n   *\n   * @param durationUs The duration of the period, in microseconds.\n   * @param isSeekable Whether seeking is supported within the period.\n   * @param isDynamic Whether the window may change when the timeline is updated.\n   * @param isLive Whether the window is live.\n   */\n  public SinglePeriodTimeline(\n      long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) {\n    this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null);\n  }\n\n  /**\n   * Creates a timeline containing a single period and a window that spans it.\n   *\n   * @param durationUs The duration of the period, in microseconds.\n   * @param isSeekable Whether seeking is supported within the period.\n   * @param isDynamic Whether the window may change when the timeline is updated.\n   * @param isLive Whether the window is live.\n   * @param manifest The manifest. May be {@code null}.\n   * @param tag A tag used for {@link Window#tag}.\n   */\n  public SinglePeriodTimeline(\n      long durationUs,\n      boolean isSeekable,\n      boolean isDynamic,\n      boolean isLive,\n      @Nullable Object manifest,\n      @Nullable Object tag) {\n    this(\n        durationUs,\n        durationUs,\n        /* windowPositionInPeriodUs= */ 0,\n        /* windowDefaultStartPositionUs= */ 0,\n        isSeekable,\n        isDynamic,\n        isLive,\n        manifest,\n        tag);\n  }\n\n  /**\n   * Creates a timeline with one period, and a window of known duration starting at a specified\n   * position in the period.\n   *\n   * @param periodDurationUs The duration of the period in microseconds.\n   * @param windowDurationUs The duration of the window in microseconds.\n   * @param windowPositionInPeriodUs The position of the start of the window in the period, in\n   *     microseconds.\n   * @param windowDefaultStartPositionUs The default position relative to the start of the window at\n   *     which to begin playback, in microseconds.\n   * @param isSeekable Whether seeking is supported within the window.\n   * @param isDynamic Whether the window may change when the timeline is updated.\n   * @param isLive Whether the window is live.\n   * @param manifest The manifest. May be (@code null}.\n   * @param tag A tag used for {@link Timeline.Window#tag}.\n   */\n  public SinglePeriodTimeline(\n      long periodDurationUs,\n      long windowDurationUs,\n      long windowPositionInPeriodUs,\n      long windowDefaultStartPositionUs,\n      boolean isSeekable,\n      boolean isDynamic,\n      boolean isLive,\n      @Nullable Object manifest,\n      @Nullable Object tag) {\n    this(\n        /* presentationStartTimeMs= */ C.TIME_UNSET,\n        /* windowStartTimeMs= */ C.TIME_UNSET,\n        periodDurationUs,\n        windowDurationUs,\n        windowPositionInPeriodUs,\n        windowDefaultStartPositionUs,\n        isSeekable,\n        isDynamic,\n        isLive,\n        manifest,\n        tag);\n  }\n\n  /**\n   * Creates a timeline with one period, and a window of known duration starting at a specified\n   * position in the period.\n   *\n   * @param presentationStartTimeMs The start time of the presentation in milliseconds since the\n   *     epoch.\n   * @param windowStartTimeMs The window's start time in milliseconds since the epoch.\n   * @param periodDurationUs The duration of the period in microseconds.\n   * @param windowDurationUs The duration of the window in microseconds.\n   * @param windowPositionInPeriodUs The position of the start of the window in the period, in\n   *     microseconds.\n   * @param windowDefaultStartPositionUs The default position relative to the start of the window at\n   *     which to begin playback, in microseconds.\n   * @param isSeekable Whether seeking is supported within the window.\n   * @param isDynamic Whether the window may change when the timeline is updated.\n   * @param isLive Whether the window is live.\n   * @param manifest The manifest. May be {@code null}.\n   * @param tag A tag used for {@link Timeline.Window#tag}.\n   */\n  public SinglePeriodTimeline(\n      long presentationStartTimeMs,\n      long windowStartTimeMs,\n      long periodDurationUs,\n      long windowDurationUs,\n      long windowPositionInPeriodUs,\n      long windowDefaultStartPositionUs,\n      boolean isSeekable,\n      boolean isDynamic,\n      boolean isLive,\n      @Nullable Object manifest,\n      @Nullable Object tag) {\n    this.presentationStartTimeMs = presentationStartTimeMs;\n    this.windowStartTimeMs = windowStartTimeMs;\n    this.periodDurationUs = periodDurationUs;\n    this.windowDurationUs = windowDurationUs;\n    this.windowPositionInPeriodUs = windowPositionInPeriodUs;\n    this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;\n    this.isSeekable = isSeekable;\n    this.isDynamic = isDynamic;\n    this.isLive = isLive;\n    this.manifest = manifest;\n    this.tag = tag;\n  }\n\n  @Override\n  public int getWindowCount() {\n    return 1;\n  }\n\n  @Override\n  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n    Assertions.checkIndex(windowIndex, 0, 1);\n    long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;\n    if (isDynamic && defaultPositionProjectionUs != 0) {\n      if (windowDurationUs == C.TIME_UNSET) {\n        // Don't allow projection into a window that has an unknown duration.\n        windowDefaultStartPositionUs = C.TIME_UNSET;\n      } else {\n        windowDefaultStartPositionUs += defaultPositionProjectionUs;\n        if (windowDefaultStartPositionUs > windowDurationUs) {\n          // The projection takes us beyond the end of the window.\n          windowDefaultStartPositionUs = C.TIME_UNSET;\n        }\n      }\n    }\n    return window.set(\n        Window.SINGLE_WINDOW_UID,\n        tag,\n        manifest,\n        presentationStartTimeMs,\n        windowStartTimeMs,\n        isSeekable,\n        isDynamic,\n        isLive,\n        windowDefaultStartPositionUs,\n        windowDurationUs,\n        0,\n        0,\n        windowPositionInPeriodUs);\n  }\n\n  @Override\n  public int getPeriodCount() {\n    return 1;\n  }\n\n  @Override\n  public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    Assertions.checkIndex(periodIndex, 0, 1);\n    Object uid = setIds ? UID : null;\n    return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs);\n  }\n\n  @Override\n  public int getIndexOfPeriod(Object uid) {\n    return UID.equals(uid) ? 0 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public Object getUidOfPeriod(int periodIndex) {\n    Assertions.checkIndex(periodIndex, 0, 1);\n    return UID;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport com.google.android.exoplayer2.upstream.StatsDataSource;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A {@link MediaPeriod} with a single sample.\n */\n/* package */ final class SingleSampleMediaPeriod implements MediaPeriod,\n    Loader.Callback<SingleSampleMediaPeriod.SourceLoadable>  {\n\n  /**\n   * The initial size of the allocation used to hold the sample data.\n   */\n  private static final int INITIAL_SAMPLE_SIZE = 1024;\n\n  private final DataSpec dataSpec;\n  private final DataSource.Factory dataSourceFactory;\n  @Nullable private final TransferListener transferListener;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final EventDispatcher eventDispatcher;\n  private final TrackGroupArray tracks;\n  private final ArrayList<SampleStreamImpl> sampleStreams;\n  private final long durationUs;\n\n  // Package private to avoid thunk methods.\n  /* package */ final Loader loader;\n  /* package */ final Format format;\n  /* package */ final boolean treatLoadErrorsAsEndOfStream;\n\n  /* package */ boolean notifiedReadingStarted;\n  /* package */ boolean loadingFinished;\n  /* package */ byte @MonotonicNonNull [] sampleData;\n  /* package */ int sampleSize;\n\n  public SingleSampleMediaPeriod(\n      DataSpec dataSpec,\n      DataSource.Factory dataSourceFactory,\n      @Nullable TransferListener transferListener,\n      Format format,\n      long durationUs,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      boolean treatLoadErrorsAsEndOfStream) {\n    this.dataSpec = dataSpec;\n    this.dataSourceFactory = dataSourceFactory;\n    this.transferListener = transferListener;\n    this.format = format;\n    this.durationUs = durationUs;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;\n    tracks = new TrackGroupArray(new TrackGroup(format));\n    sampleStreams = new ArrayList<>();\n    loader = new Loader(\"Loader:SingleSampleMediaPeriod\");\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  public void release() {\n    loader.release();\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    callback.onPrepared(this);\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return tracks;\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n        sampleStreams.remove(streams[i]);\n        streams[i] = null;\n      }\n      if (streams[i] == null && selections[i] != null) {\n        SampleStreamImpl stream = new SampleStreamImpl();\n        sampleStreams.add(stream);\n        streams[i] = stream;\n        streamResetFlags[i] = true;\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    // Do nothing.\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    // Do nothing.\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {\n      return false;\n    }\n    DataSource dataSource = dataSourceFactory.createDataSource();\n    if (transferListener != null) {\n      dataSource.addTransferListener(transferListener);\n    }\n    long elapsedRealtimeMs =\n        loader.startLoading(\n            new SourceLoadable(dataSpec, dataSource),\n            /* callback= */ this,\n            loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA));\n    eventDispatcher.loadStarted(\n        dataSpec,\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        format,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ 0,\n        durationUs,\n        elapsedRealtimeMs);\n    return true;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return loader.isLoading();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    return C.TIME_UNSET;\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return loadingFinished || loader.isLoading() ? C.TIME_END_OF_SOURCE : 0;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return loadingFinished ? C.TIME_END_OF_SOURCE : 0;\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    for (int i = 0; i < sampleStreams.size(); i++) {\n      sampleStreams.get(i).reset();\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return positionUs;\n  }\n\n  // Loader.Callback implementation.\n\n  @Override\n  public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,\n      long loadDurationMs) {\n    sampleSize = (int) loadable.dataSource.getBytesRead();\n    sampleData = Assertions.checkNotNull(loadable.sampleData);\n    loadingFinished = true;\n    eventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        format,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ 0,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        sampleSize);\n  }\n\n  @Override\n  public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,\n      boolean released) {\n    eventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        /* trackFormat= */ null,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ 0,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.dataSource.getBytesRead());\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      SourceLoadable loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long retryDelay =\n        loadErrorHandlingPolicy.getRetryDelayMsFor(\n            C.DATA_TYPE_MEDIA, loadDurationMs, error, errorCount);\n    boolean errorCanBePropagated =\n        retryDelay == C.TIME_UNSET\n            || errorCount\n                >= loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA);\n\n    LoadErrorAction action;\n    if (treatLoadErrorsAsEndOfStream && errorCanBePropagated) {\n      loadingFinished = true;\n      action = Loader.DONT_RETRY;\n    } else {\n      action =\n          retryDelay != C.TIME_UNSET\n              ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay)\n              : Loader.DONT_RETRY_FATAL;\n    }\n    eventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.dataSource.getLastOpenedUri(),\n        loadable.dataSource.getLastResponseHeaders(),\n        C.DATA_TYPE_MEDIA,\n        C.TRACK_TYPE_UNKNOWN,\n        format,\n        C.SELECTION_REASON_UNKNOWN,\n        /* trackSelectionData= */ null,\n        /* mediaStartTimeUs= */ 0,\n        durationUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.dataSource.getBytesRead(),\n        error,\n        /* wasCanceled= */ !action.isRetry());\n    return action;\n  }\n\n  private final class SampleStreamImpl implements SampleStream {\n\n    private static final int STREAM_STATE_SEND_FORMAT = 0;\n    private static final int STREAM_STATE_SEND_SAMPLE = 1;\n    private static final int STREAM_STATE_END_OF_STREAM = 2;\n\n    private int streamState;\n    private boolean notifiedDownstreamFormat;\n\n    public void reset() {\n      if (streamState == STREAM_STATE_END_OF_STREAM) {\n        streamState = STREAM_STATE_SEND_SAMPLE;\n      }\n    }\n\n    @Override\n    public boolean isReady() {\n      return loadingFinished;\n    }\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      if (!treatLoadErrorsAsEndOfStream) {\n        loader.maybeThrowError();\n      }\n    }\n\n    @Override\n    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n        boolean requireFormat) {\n      maybeNotifyDownstreamFormat();\n      if (streamState == STREAM_STATE_END_OF_STREAM) {\n        buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n        return C.RESULT_BUFFER_READ;\n      } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) {\n        formatHolder.format = format;\n        streamState = STREAM_STATE_SEND_SAMPLE;\n        return C.RESULT_FORMAT_READ;\n      } else if (loadingFinished) {\n        if (sampleData != null) {\n          buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);\n          buffer.timeUs = 0;\n          if (buffer.isFlagsOnly()) {\n            return C.RESULT_BUFFER_READ;\n          }\n          buffer.ensureSpaceForWrite(sampleSize);\n          buffer.data.put(sampleData, 0, sampleSize);\n        } else {\n          buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n        }\n        streamState = STREAM_STATE_END_OF_STREAM;\n        return C.RESULT_BUFFER_READ;\n      }\n      return C.RESULT_NOTHING_READ;\n    }\n\n    @Override\n    public int skipData(long positionUs) {\n      maybeNotifyDownstreamFormat();\n      if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {\n        streamState = STREAM_STATE_END_OF_STREAM;\n        return 1;\n      }\n      return 0;\n    }\n\n    private void maybeNotifyDownstreamFormat() {\n      if (!notifiedDownstreamFormat) {\n        eventDispatcher.downstreamFormatChanged(\n            MimeTypes.getTrackType(format.sampleMimeType),\n            format,\n            C.SELECTION_REASON_UNKNOWN,\n            /* trackSelectionData= */ null,\n            /* mediaTimeUs= */ 0);\n        notifiedDownstreamFormat = true;\n      }\n    }\n  }\n\n  /* package */ static final class SourceLoadable implements Loadable {\n\n    public final DataSpec dataSpec;\n\n    private final StatsDataSource dataSource;\n\n    @Nullable private byte[] sampleData;\n\n    // the constructor does not initialize fields: sampleData\n    @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n    public SourceLoadable(DataSpec dataSpec, DataSource dataSource) {\n      this.dataSpec = dataSpec;\n      this.dataSource = new StatsDataSource(dataSource);\n    }\n\n    @Override\n    public void cancelLoad() {\n      // Never happens.\n    }\n\n    @Override\n    public void load() throws IOException, InterruptedException {\n      // We always load from the beginning, so reset bytesRead to 0.\n      dataSource.resetBytesRead();\n      try {\n        // Create and open the input.\n        dataSource.open(dataSpec);\n        // Load the sample data.\n        int result = 0;\n        while (result != C.RESULT_END_OF_INPUT) {\n          int sampleSize = (int) dataSource.getBytesRead();\n          if (sampleData == null) {\n            sampleData = new byte[INITIAL_SAMPLE_SIZE];\n          } else if (sampleSize == sampleData.length) {\n            sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);\n          }\n          result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\n\n/**\n * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}.\n */\npublic final class SingleSampleMediaSource extends BaseMediaSource {\n\n  /**\n   * Listener of {@link SingleSampleMediaSource} events.\n   *\n   * @deprecated Use {@link MediaSourceEventListener}.\n   */\n  @Deprecated\n  public interface EventListener {\n\n    /**\n     * Called when an error occurs loading media data.\n     *\n     * @param sourceId The id of the reporting {@link SingleSampleMediaSource}.\n     * @param e The cause of the failure.\n     */\n    void onLoadError(int sourceId, IOException e);\n\n  }\n\n  /** Factory for {@link SingleSampleMediaSource}. */\n  public static final class Factory {\n\n    private final DataSource.Factory dataSourceFactory;\n\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private boolean treatLoadErrorsAsEndOfStream;\n    private boolean isCreateCalled;\n    @Nullable private Object tag;\n\n    /**\n     * Creates a factory for {@link SingleSampleMediaSource}s.\n     *\n     * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will\n     *     be obtained.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory);\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link Timeline} of the source\n     * as {@link Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTag(Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the minimum number of times to retry if a loading error occurs. See {@link\n     * #setLoadErrorHandlingPolicy} for the default value.\n     *\n     * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\n     * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\n     * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\n     *\n     * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\n     */\n    @Deprecated\n    public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\n      return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /**\n     * Sets whether load errors will be treated as end-of-stream signal (load errors will not be\n     * propagated). The default value is false.\n     *\n     * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample\n     *     streams, treating them as ended instead. If false, load errors will be propagated\n     *     normally by {@link SampleStream#maybeThrowError()}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) {\n      Assertions.checkState(!isCreateCalled);\n      this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;\n      return this;\n    }\n\n    /**\n     * Returns a new {@link SingleSampleMediaSource} using the current parameters.\n     *\n     * @param uri The {@link Uri}.\n     * @param format The {@link Format} of the media stream.\n     * @param durationUs The duration of the media stream in microseconds.\n     * @return The new {@link SingleSampleMediaSource}.\n     */\n    public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) {\n      isCreateCalled = true;\n      return new SingleSampleMediaSource(\n          uri,\n          dataSourceFactory,\n          format,\n          durationUs,\n          loadErrorHandlingPolicy,\n          treatLoadErrorsAsEndOfStream,\n          tag);\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(Uri, Format, long)} and {@link\n     *     #addEventListener(Handler, MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public SingleSampleMediaSource createMediaSource(\n        Uri uri,\n        Format format,\n        long durationUs,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      SingleSampleMediaSource mediaSource = createMediaSource(uri, format, durationUs);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n  }\n\n  private final DataSpec dataSpec;\n  private final DataSource.Factory dataSourceFactory;\n  private final Format format;\n  private final long durationUs;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final boolean treatLoadErrorsAsEndOfStream;\n  private final Timeline timeline;\n  @Nullable private final Object tag;\n\n  @Nullable private TransferListener transferListener;\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will\n   *     be obtained.\n   * @param format The {@link Format} associated with the output track.\n   * @param durationUs The duration of the media stream in microseconds.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SingleSampleMediaSource(\n      Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {\n    this(\n        uri,\n        dataSourceFactory,\n        format,\n        durationUs,\n        DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT);\n  }\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will\n   *     be obtained.\n   * @param format The {@link Format} associated with the output track.\n   * @param durationUs The duration of the media stream in microseconds.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public SingleSampleMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      Format format,\n      long durationUs,\n      int minLoadableRetryCount) {\n    this(\n        uri,\n        dataSourceFactory,\n        format,\n        durationUs,\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        /* treatLoadErrorsAsEndOfStream= */ false,\n        /* tag= */ null);\n  }\n\n  /**\n   * @param uri The {@link Uri} of the media stream.\n   * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will\n   *     be obtained.\n   * @param format The {@link Format} associated with the output track.\n   * @param durationUs The duration of the media stream in microseconds.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param eventSourceId An identifier that gets passed to {@code eventListener} methods.\n   * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample\n   *     streams, treating them as ended instead. If false, load errors will be propagated normally\n   *     by {@link SampleStream#maybeThrowError()}.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SingleSampleMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      Format format,\n      long durationUs,\n      int minLoadableRetryCount,\n      Handler eventHandler,\n      EventListener eventListener,\n      int eventSourceId,\n      boolean treatLoadErrorsAsEndOfStream) {\n    this(\n        uri,\n        dataSourceFactory,\n        format,\n        durationUs,\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        treatLoadErrorsAsEndOfStream,\n        /* tag= */ null);\n    if (eventHandler != null && eventListener != null) {\n      addEventListener(eventHandler, new EventListenerWrapper(eventListener, eventSourceId));\n    }\n  }\n\n  private SingleSampleMediaSource(\n      Uri uri,\n      DataSource.Factory dataSourceFactory,\n      Format format,\n      long durationUs,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      boolean treatLoadErrorsAsEndOfStream,\n      @Nullable Object tag) {\n    this.dataSourceFactory = dataSourceFactory;\n    this.format = format;\n    this.durationUs = durationUs;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;\n    this.tag = tag;\n    dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP);\n    timeline =\n        new SinglePeriodTimeline(\n            durationUs,\n            /* isSeekable= */ true,\n            /* isDynamic= */ false,\n            /* isLive= */ false,\n            /* manifest= */ null,\n            tag);\n  }\n\n  // MediaSource implementation.\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    transferListener = mediaTransferListener;\n    refreshSourceInfo(timeline);\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    return new SingleSampleMediaPeriod(\n        dataSpec,\n        dataSourceFactory,\n        transferListener,\n        format,\n        durationUs,\n        loadErrorHandlingPolicy,\n        createEventDispatcher(id),\n        treatLoadErrorsAsEndOfStream);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    ((SingleSampleMediaPeriod) mediaPeriod).release();\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    // Do nothing.\n  }\n\n  /**\n   * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in\n   * {@link MediaSourceEventListener}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  private static final class EventListenerWrapper implements MediaSourceEventListener {\n\n    private final EventListener eventListener;\n    private final int eventSourceId;\n\n    public EventListenerWrapper(EventListener eventListener, int eventSourceId) {\n      this.eventListener = Assertions.checkNotNull(eventListener);\n      this.eventSourceId = eventSourceId;\n    }\n\n    @Override\n    public void onLoadError(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData,\n        IOException error,\n        boolean wasCanceled) {\n      eventListener.onLoadError(eventSourceId, error);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Arrays;\n\n// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction\n// does not apply.\n/**\n * Defines a group of tracks exposed by a {@link MediaPeriod}.\n *\n * <p>A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a\n * group at any given time, however this {@link SampleStream} may adapt between multiple tracks\n * within the group.\n */\npublic final class TrackGroup implements Parcelable {\n\n  /**\n   * The number of tracks in the group.\n   */\n  public final int length;\n\n  private final Format[] formats;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /**\n   * @param formats The track formats. Must not be null, contain null elements or be of length 0.\n   */\n  public TrackGroup(Format... formats) {\n    Assertions.checkState(formats.length > 0);\n    this.formats = formats;\n    this.length = formats.length;\n  }\n\n  /* package */ TrackGroup(Parcel in) {\n    length = in.readInt();\n    formats = new Format[length];\n    for (int i = 0; i < length; i++) {\n      formats[i] = in.readParcelable(Format.class.getClassLoader());\n    }\n  }\n\n  /**\n   * Returns the format of the track at a given index.\n   *\n   * @param index The index of the track.\n   * @return The track's format.\n   */\n  public Format getFormat(int index) {\n    return formats[index];\n  }\n\n  /**\n   * Returns the index of the track with the given format in the group. The format is located by\n   * identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if\n   * multiple tracks have formats that contain the same values.\n   *\n   * @param format The format.\n   * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  public int indexOf(Format format) {\n    for (int i = 0; i < formats.length; i++) {\n      if (format == formats[i]) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 17;\n      result = 31 * result + Arrays.hashCode(formats);\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    TrackGroup other = (TrackGroup) obj;\n    return length == other.length && Arrays.equals(formats, other.formats);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(length);\n    for (int i = 0; i < length; i++) {\n      dest.writeParcelable(formats[i], 0);\n    }\n  }\n\n  public static final Creator<TrackGroup> CREATOR =\n      new Creator<TrackGroup>() {\n\n        @Override\n        public TrackGroup createFromParcel(Parcel in) {\n          return new TrackGroup(in);\n        }\n\n        @Override\n        public TrackGroup[] newArray(int size) {\n          return new TrackGroup[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.util.Arrays;\n\n/** An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. */\npublic final class TrackGroupArray implements Parcelable {\n\n  /**\n   * The empty array.\n   */\n  public static final TrackGroupArray EMPTY = new TrackGroupArray();\n\n  /**\n   * The number of groups in the array. Greater than or equal to zero.\n   */\n  public final int length;\n\n  private final TrackGroup[] trackGroups;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /**\n   * @param trackGroups The groups. Must not be null or contain null elements, but may be empty.\n   */\n  public TrackGroupArray(TrackGroup... trackGroups) {\n    this.trackGroups = trackGroups;\n    this.length = trackGroups.length;\n  }\n\n  /* package */ TrackGroupArray(Parcel in) {\n    length = in.readInt();\n    trackGroups = new TrackGroup[length];\n    for (int i = 0; i < length; i++) {\n      trackGroups[i] = in.readParcelable(TrackGroup.class.getClassLoader());\n    }\n  }\n\n  /**\n   * Returns the group at a given index.\n   *\n   * @param index The index of the group.\n   * @return The group.\n   */\n  public TrackGroup get(int index) {\n    return trackGroups[index];\n  }\n\n  /**\n   * Returns the index of a group within the array.\n   *\n   * @param group The group.\n   * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  public int indexOf(TrackGroup group) {\n    for (int i = 0; i < length; i++) {\n      // Suppressed reference equality warning because this is looking for the index of a specific\n      // TrackGroup object, not the index of a potential equal TrackGroup.\n      if (trackGroups[i] == group) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns whether this track group array is empty.\n   */\n  public boolean isEmpty() {\n    return length == 0;\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      hashCode = Arrays.hashCode(trackGroups);\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    TrackGroupArray other = (TrackGroupArray) obj;\n    return length == other.length && Arrays.equals(trackGroups, other.trackGroups);\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(length);\n    for (int i = 0; i < length; i++) {\n      dest.writeParcelable(trackGroups[i], 0);\n    }\n  }\n\n  public static final Creator<TrackGroupArray> CREATOR =\n      new Creator<TrackGroupArray>() {\n\n        @Override\n        public TrackGroupArray createFromParcel(Parcel in) {\n          return new TrackGroupArray(in);\n        }\n\n        @Override\n        public TrackGroupArray[] newArray(int size) {\n          return new TrackGroupArray[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.ParserException;\n\n/**\n * Thrown if the input format was not recognized.\n */\npublic class UnrecognizedInputFormatException extends ParserException {\n\n  /**\n   * The {@link Uri} from which the unrecognized data was read.\n   */\n  public final Uri uri;\n\n  /**\n   * @param message The detail message for the exception.\n   * @param uri The {@link Uri} from which the unrecognized data was read.\n   */\n  public UnrecognizedInputFormatException(String message, Uri uri) {\n    super(message);\n    this.uri = uri;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.ads;\n\nimport android.net.Uri;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Represents ad group times relative to the start of the media and information on the state and\n * URIs of ads within each ad group.\n *\n * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the\n * required changes.\n */\npublic final class AdPlaybackState {\n\n  /**\n   * Represents a group of ads, with information about their states.\n   *\n   * <p>Instances are immutable. Call the {@code with*} methods to get new instances that have the\n   * required changes.\n   */\n  public static final class AdGroup {\n\n    /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */\n    public final int count;\n    /** The URI of each ad in the ad group. */\n    public final @NullableType Uri[] uris;\n    /** The state of each ad in the ad group. */\n    @AdState public final int[] states;\n    /** The durations of each ad in the ad group, in microseconds. */\n    public final long[] durationsUs;\n\n    /** Creates a new ad group with an unspecified number of ads. */\n    public AdGroup() {\n      this(\n          /* count= */ C.LENGTH_UNSET,\n          /* states= */ new int[0],\n          /* uris= */ new Uri[0],\n          /* durationsUs= */ new long[0]);\n    }\n\n    private AdGroup(\n        int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) {\n      Assertions.checkArgument(states.length == uris.length);\n      this.count = count;\n      this.states = states;\n      this.uris = uris;\n      this.durationsUs = durationsUs;\n    }\n\n    /**\n     * Returns the index of the first ad in the ad group that should be played, or {@link #count} if\n     * no ads should be played.\n     */\n    public int getFirstAdIndexToPlay() {\n      return getNextAdIndexToPlay(-1);\n    }\n\n    /**\n     * Returns the index of the next ad in the ad group that should be played after playing {@code\n     * lastPlayedAdIndex}, or {@link #count} if no later ads should be played.\n     */\n    public int getNextAdIndexToPlay(int lastPlayedAdIndex) {\n      int nextAdIndexToPlay = lastPlayedAdIndex + 1;\n      while (nextAdIndexToPlay < states.length) {\n        if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE\n            || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {\n          break;\n        }\n        nextAdIndexToPlay++;\n      }\n      return nextAdIndexToPlay;\n    }\n\n    /** Returns whether the ad group has at least one ad that still needs to be played. */\n    public boolean hasUnplayedAds() {\n      return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object o) {\n      if (this == o) {\n        return true;\n      }\n      if (o == null || getClass() != o.getClass()) {\n        return false;\n      }\n      AdGroup adGroup = (AdGroup) o;\n      return count == adGroup.count\n          && Arrays.equals(uris, adGroup.uris)\n          && Arrays.equals(states, adGroup.states)\n          && Arrays.equals(durationsUs, adGroup.durationsUs);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = count;\n      result = 31 * result + Arrays.hashCode(uris);\n      result = 31 * result + Arrays.hashCode(states);\n      result = 31 * result + Arrays.hashCode(durationsUs);\n      return result;\n    }\n\n    /**\n     * Returns a new instance with the ad count set to {@code count}. This method may only be called\n     * if this instance's ad count has not yet been specified.\n     */\n    @CheckResult\n    public AdGroup withAdCount(int count) {\n      Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);\n      @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);\n      long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);\n      @NullableType Uri[] uris = Arrays.copyOf(this.uris, count);\n      return new AdGroup(count, states, uris, durationsUs);\n    }\n\n    /**\n     * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad\n     * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link\n     * #AD_STATE_UNAVAILABLE}, which is the default state.\n     *\n     * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the\n     * ad count specified later. Otherwise, {@code index} must be less than the current ad count.\n     */\n    @CheckResult\n    public AdGroup withAdUri(Uri uri, int index) {\n      Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);\n      @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);\n      Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);\n      long[] durationsUs =\n          this.durationsUs.length == states.length\n              ? this.durationsUs\n              : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);\n      @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length);\n      uris[index] = uri;\n      states[index] = AD_STATE_AVAILABLE;\n      return new AdGroup(count, states, uris, durationsUs);\n    }\n\n    /**\n     * Returns a new instance with the specified ad set to the specified {@code state}. The ad\n     * specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link\n     * #AD_STATE_AVAILABLE}.\n     *\n     * <p>This instance's ad count may be unknown, in which case {@code index} must be less than the\n     * ad count specified later. Otherwise, {@code index} must be less than the current ad count.\n     */\n    @CheckResult\n    public AdGroup withAdState(@AdState int state, int index) {\n      Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);\n      @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);\n      Assertions.checkArgument(\n          states[index] == AD_STATE_UNAVAILABLE\n              || states[index] == AD_STATE_AVAILABLE\n              || states[index] == state);\n      long[] durationsUs =\n          this.durationsUs.length == states.length\n              ? this.durationsUs\n              : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);\n      @NullableType\n      Uri[] uris =\n          this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);\n      states[index] = state;\n      return new AdGroup(count, states, uris, durationsUs);\n    }\n\n    /** Returns a new instance with the specified ad durations, in microseconds. */\n    @CheckResult\n    public AdGroup withAdDurationsUs(long[] durationsUs) {\n      Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);\n      if (durationsUs.length < this.uris.length) {\n        durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);\n      }\n      return new AdGroup(count, states, uris, durationsUs);\n    }\n\n    /**\n     * Returns an instance with all unavailable and available ads marked as skipped. If the ad count\n     * hasn't been set, it will be set to zero.\n     */\n    @CheckResult\n    public AdGroup withAllAdsSkipped() {\n      if (count == C.LENGTH_UNSET) {\n        return new AdGroup(\n            /* count= */ 0,\n            /* states= */ new int[0],\n            /* uris= */ new Uri[0],\n            /* durationsUs= */ new long[0]);\n      }\n      int count = this.states.length;\n      @AdState int[] states = Arrays.copyOf(this.states, count);\n      for (int i = 0; i < count; i++) {\n        if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) {\n          states[i] = AD_STATE_SKIPPED;\n        }\n      }\n      return new AdGroup(count, states, uris, durationsUs);\n    }\n\n    @CheckResult\n    private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {\n      int oldStateCount = states.length;\n      int newStateCount = Math.max(count, oldStateCount);\n      states = Arrays.copyOf(states, newStateCount);\n      Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE);\n      return states;\n    }\n\n    @CheckResult\n    private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) {\n      int oldDurationsUsCount = durationsUs.length;\n      int newDurationsUsCount = Math.max(count, oldDurationsUsCount);\n      durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount);\n      Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);\n      return durationsUs;\n    }\n  }\n\n  /**\n   * Represents the state of an ad in an ad group. One of {@link #AD_STATE_UNAVAILABLE}, {@link\n   * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link\n   * #AD_STATE_ERROR}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    AD_STATE_UNAVAILABLE,\n    AD_STATE_AVAILABLE,\n    AD_STATE_SKIPPED,\n    AD_STATE_PLAYED,\n    AD_STATE_ERROR,\n  })\n  public @interface AdState {}\n  /** State for an ad that does not yet have a URL. */\n  public static final int AD_STATE_UNAVAILABLE = 0;\n  /** State for an ad that has a URL but has not yet been played. */\n  public static final int AD_STATE_AVAILABLE = 1;\n  /** State for an ad that was skipped. */\n  public static final int AD_STATE_SKIPPED = 2;\n  /** State for an ad that was played in full. */\n  public static final int AD_STATE_PLAYED = 3;\n  /** State for an ad that could not be loaded. */\n  public static final int AD_STATE_ERROR = 4;\n\n  /** Ad playback state with no ads. */\n  public static final AdPlaybackState NONE = new AdPlaybackState();\n\n  /** The number of ad groups. */\n  public final int adGroupCount;\n  /**\n   * The times of ad groups, in microseconds. A final element with the value {@link\n   * C#TIME_END_OF_SOURCE} indicates a postroll ad.\n   */\n  public final long[] adGroupTimesUs;\n  /** The ad groups. */\n  public final AdGroup[] adGroups;\n  /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */\n  public final long adResumePositionUs;\n  /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */\n  public final long contentDurationUs;\n\n  /**\n   * Creates a new ad playback state with the specified ad group times.\n   *\n   * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value\n   *     {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.\n   */\n  public AdPlaybackState(long... adGroupTimesUs) {\n    int count = adGroupTimesUs.length;\n    adGroupCount = count;\n    this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);\n    this.adGroups = new AdGroup[count];\n    for (int i = 0; i < count; i++) {\n      adGroups[i] = new AdGroup();\n    }\n    adResumePositionUs = 0;\n    contentDurationUs = C.TIME_UNSET;\n  }\n\n  private AdPlaybackState(\n      long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {\n    adGroupCount = adGroups.length;\n    this.adGroupTimesUs = adGroupTimesUs;\n    this.adGroups = adGroups;\n    this.adResumePositionUs = adResumePositionUs;\n    this.contentDurationUs = contentDurationUs;\n  }\n\n  /**\n   * Returns the index of the ad group at or before {@code positionUs}, if that ad group is\n   * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no\n   * ads remaining to be played, or if there is no such ad group.\n   *\n   * @param positionUs The position at or before which to find an ad group, in microseconds, or\n   *     {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any\n   *     unplayed postroll ad group will be returned).\n   * @return The index of the ad group, or {@link C#INDEX_UNSET}.\n   */\n  public int getAdGroupIndexForPositionUs(long positionUs) {\n    // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.\n    // In practice we expect there to be few ad groups so the search shouldn't be expensive.\n    int index = adGroupTimesUs.length - 1;\n    while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) {\n      index--;\n    }\n    return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be\n   * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.\n   *\n   * @param positionUs The position after which to find an ad group, in microseconds, or {@link\n   *     C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group\n   *     after the position).\n   * @param periodDurationUs The duration of the containing period in microseconds, or {@link\n   *     C#TIME_UNSET} if not known.\n   * @return The index of the ad group, or {@link C#INDEX_UNSET}.\n   */\n  public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) {\n    if (positionUs == C.TIME_END_OF_SOURCE\n        || (periodDurationUs != C.TIME_UNSET && positionUs >= periodDurationUs)) {\n      return C.INDEX_UNSET;\n    }\n    // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.\n    // In practice we expect there to be few ad groups so the search shouldn't be expensive.\n    int index = 0;\n    while (index < adGroupTimesUs.length\n        && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE\n        && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) {\n      index++;\n    }\n    return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.\n   * The ad count must be greater than zero.\n   */\n  @CheckResult\n  public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {\n    Assertions.checkArgument(adCount > 0);\n    if (adGroups[adGroupIndex].count == adCount) {\n      return this;\n    }\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad URI. */\n  @CheckResult\n  public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup);\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad marked as played. */\n  @CheckResult\n  public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup);\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad marked as skipped. */\n  @CheckResult\n  public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad marked as having a load error. */\n  @CheckResult\n  public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup);\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /**\n   * Returns an instance with all ads in the specified ad group skipped (except for those already\n   * marked as played or in the error state).\n   */\n  @CheckResult\n  public AdPlaybackState withSkippedAdGroup(int adGroupIndex) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped();\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad durations, in microseconds. */\n  @CheckResult\n  public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {\n    AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);\n    for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {\n      adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);\n    }\n    return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n  }\n\n  /** Returns an instance with the specified ad resume position, in microseconds. */\n  @CheckResult\n  public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {\n    if (this.adResumePositionUs == adResumePositionUs) {\n      return this;\n    } else {\n      return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n    }\n  }\n\n  /** Returns an instance with the specified content duration, in microseconds. */\n  @CheckResult\n  public AdPlaybackState withContentDurationUs(long contentDurationUs) {\n    if (this.contentDurationUs == contentDurationUs) {\n      return this;\n    } else {\n      return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);\n    }\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    AdPlaybackState that = (AdPlaybackState) o;\n    return adGroupCount == that.adGroupCount\n        && adResumePositionUs == that.adResumePositionUs\n        && contentDurationUs == that.contentDurationUs\n        && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs)\n        && Arrays.equals(adGroups, that.adGroups);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = adGroupCount;\n    result = 31 * result + (int) adResumePositionUs;\n    result = 31 * result + (int) contentDurationUs;\n    result = 31 * result + Arrays.hashCode(adGroupTimesUs);\n    result = 31 * result + Arrays.hashCode(adGroups);\n    return result;\n  }\n\n  private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) {\n    if (positionUs == C.TIME_END_OF_SOURCE) {\n      // The end of the content is at (but not before) any postroll ad, and after any other ads.\n      return false;\n    }\n    long adGroupPositionUs = adGroupTimesUs[adGroupIndex];\n    if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {\n      return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs;\n    } else {\n      return positionUs < adGroupPositionUs;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.ads;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.IOException;\n\n/**\n * Interface for loaders of ads, which can be used with {@link AdsMediaSource}.\n *\n * <p>Ads loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In\n * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)}\n * with a new copy of the current {@link AdPlaybackState} whenever further information about ads\n * becomes known (for example, when an ad media URI is available, or an ad has played to the end).\n *\n * <p>{@link #start(EventListener, AdViewProvider)} will be called when the ads media source first\n * initializes, at which point the loader can request ads. If the player enters the background,\n * {@link #stop()} will be called. Loaders should maintain any ad playback state in preparation for\n * a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the\n * player is detached, update the ad playback state with the current playback position using {@link\n * AdPlaybackState#withAdResumePositionUs(long)}.\n *\n * <p>If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the\n * implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener\n * to provide the existing playback state to the new player.\n */\npublic interface AdsLoader {\n\n  /** Listener for ads loader events. All methods are called on the main thread. */\n  interface EventListener {\n\n    /**\n     * Called when the ad playback state has been updated.\n     *\n     * @param adPlaybackState The new ad playback state.\n     */\n    default void onAdPlaybackState(AdPlaybackState adPlaybackState) {}\n\n    /**\n     * Called when there was an error loading ads.\n     *\n     * @param error The error.\n     * @param dataSpec The data spec associated with the load error.\n     */\n    default void onAdLoadError(AdLoadException error, DataSpec dataSpec) {}\n\n    /** Called when the user clicks through an ad (for example, following a 'learn more' link). */\n    default void onAdClicked() {}\n\n    /** Called when the user taps a non-clickthrough part of an ad. */\n    default void onAdTapped() {}\n  }\n\n  /** Provides views for the ad UI. */\n  interface AdViewProvider {\n\n    /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */\n    ViewGroup getAdViewGroup();\n\n    /**\n     * Returns an array of views that are shown on top of the ad view group, but that are essential\n     * for controlling playback and should be excluded from ad viewability measurements by the\n     * {@link AdsLoader} (if it supports this).\n     *\n     * <p>Each view must be either a fully transparent overlay (for capturing touch events), or a\n     * small piece of transient UI that is essential to the user experience of playback (such as a\n     * button to pause/resume playback or a transient full-screen or cast button). For more\n     * information see the documentation for your ads loader.\n     */\n    View[] getAdOverlayViews();\n  }\n\n  // Methods called by the application.\n\n  /**\n   * Sets the player that will play the loaded ads.\n   *\n   * <p>This method must be called before the player is prepared with media using this ads loader.\n   *\n   * <p>This method must also be called on the main thread and only players which are accessed on\n   * the main thread are supported ({@code player.getApplicationLooper() ==\n   * Looper.getMainLooper()}).\n   *\n   * @param player The player instance that will play the loaded ads. May be null to delete the\n   *     reference to a previously set player.\n   */\n  void setPlayer(@Nullable Player player);\n\n  /**\n   * Releases the loader. Must be called by the application on the main thread when the instance is\n   * no longer needed.\n   */\n  void release();\n\n  // Methods called by AdsMediaSource.\n\n  /**\n   * Sets the supported content types for ad media. Must be called before the first call to {@link\n   * #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main\n   * thread by {@link AdsMediaSource}.\n   *\n   * @param contentTypes The supported content types for ad media. Each element must be one of\n   *     {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.\n   */\n  void setSupportedContentTypes(@C.ContentType int... contentTypes);\n\n  /**\n   * Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.\n   *\n   * @param eventListener Listener for ads loader events.\n   * @param adViewProvider Provider of views for the ad UI.\n   */\n  void start(EventListener eventListener, AdViewProvider adViewProvider);\n\n  /**\n   * Stops using the ads loader for playback and deregisters the event listener. Called on the main\n   * thread by {@link AdsMediaSource}.\n   */\n  void stop();\n\n  /**\n   * Notifies the ads loader that the player was not able to prepare media for a given ad.\n   * Implementations should update the ad playback state as the specified ad has failed to load.\n   * Called on the main thread by {@link AdsMediaSource}.\n   *\n   * @param adGroupIndex The index of the ad group.\n   * @param adIndexInAdGroup The index of the ad in the ad group.\n   * @param exception The preparation error.\n   */\n  void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.ads;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.CompositeMediaSource;\nimport com.google.android.exoplayer2.source.MaskingMediaPeriod;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;\nimport com.google.android.exoplayer2.source.MediaSourceFactory;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source\n * cannot be used as a child source in a composition. It must be the top-level source used to\n * prepare the player.\n */\npublic final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {\n\n  /**\n   * Wrapper for exceptions that occur while loading ads, which are notified via {@link\n   * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData,\n   * IOException, boolean)}.\n   */\n  public static final class AdLoadException extends IOException {\n\n    /**\n     * Types of ad load exceptions. One of {@link #TYPE_AD}, {@link #TYPE_AD_GROUP}, {@link\n     * #TYPE_ALL_ADS} or {@link #TYPE_UNEXPECTED}.\n     */\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED})\n    public @interface Type {}\n    /** Type for when an ad failed to load. The ad will be skipped. */\n    public static final int TYPE_AD = 0;\n    /** Type for when an ad group failed to load. The ad group will be skipped. */\n    public static final int TYPE_AD_GROUP = 1;\n    /** Type for when all ad groups failed to load. All ads will be skipped. */\n    public static final int TYPE_ALL_ADS = 2;\n    /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */\n    public static final int TYPE_UNEXPECTED = 3;\n\n    /** Returns a new ad load exception of {@link #TYPE_AD}. */\n    public static AdLoadException createForAd(Exception error) {\n      return new AdLoadException(TYPE_AD, error);\n    }\n\n    /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */\n    public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) {\n      return new AdLoadException(\n          TYPE_AD_GROUP, new IOException(\"Failed to load ad group \" + adGroupIndex, error));\n    }\n\n    /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */\n    public static AdLoadException createForAllAds(Exception error) {\n      return new AdLoadException(TYPE_ALL_ADS, error);\n    }\n\n    /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */\n    public static AdLoadException createForUnexpected(RuntimeException error) {\n      return new AdLoadException(TYPE_UNEXPECTED, error);\n    }\n\n    /** The {@link Type} of the ad load exception. */\n    public final @Type int type;\n\n    private AdLoadException(@Type int type, Exception cause) {\n      super(cause);\n      this.type = type;\n    }\n\n    /**\n     * Returns the {@link RuntimeException} that caused the exception if its type is {@link\n     * #TYPE_UNEXPECTED}.\n     */\n    public RuntimeException getRuntimeExceptionForUnexpected() {\n      Assertions.checkState(type == TYPE_UNEXPECTED);\n      return (RuntimeException) Assertions.checkNotNull(getCause());\n    }\n  }\n\n  // Used to identify the content \"child\" source for CompositeMediaSource.\n  private static final MediaPeriodId DUMMY_CONTENT_MEDIA_PERIOD_ID =\n      new MediaPeriodId(/* periodUid= */ new Object());\n\n  private final MediaSource contentMediaSource;\n  private final MediaSourceFactory adMediaSourceFactory;\n  private final AdsLoader adsLoader;\n  private final AdsLoader.AdViewProvider adViewProvider;\n  private final Handler mainHandler;\n  private final Map<MediaSource, List<MaskingMediaPeriod>> maskingMediaPeriodByAdMediaSource;\n  private final Timeline.Period period;\n\n  // Accessed on the player thread.\n  @Nullable private ComponentListener componentListener;\n  @Nullable private Timeline contentTimeline;\n  @Nullable private AdPlaybackState adPlaybackState;\n  private @NullableType MediaSource[][] adGroupMediaSources;\n  private @NullableType Timeline[][] adGroupTimelines;\n\n  /**\n   * Constructs a new source that inserts ads linearly with the content specified by {@code\n   * contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}.\n   *\n   * @param contentMediaSource The {@link MediaSource} providing the content to play.\n   * @param dataSourceFactory Factory for data sources used to load ad media.\n   * @param adsLoader The loader for ads.\n   * @param adViewProvider Provider of views for the ad UI.\n   */\n  public AdsMediaSource(\n      MediaSource contentMediaSource,\n      DataSource.Factory dataSourceFactory,\n      AdsLoader adsLoader,\n      AdsLoader.AdViewProvider adViewProvider) {\n    this(\n        contentMediaSource,\n        new ProgressiveMediaSource.Factory(dataSourceFactory),\n        adsLoader,\n        adViewProvider);\n  }\n\n  /**\n   * Constructs a new source that inserts ads linearly with the content specified by {@code\n   * contentMediaSource}.\n   *\n   * @param contentMediaSource The {@link MediaSource} providing the content to play.\n   * @param adMediaSourceFactory Factory for media sources used to load ad media.\n   * @param adsLoader The loader for ads.\n   * @param adViewProvider Provider of views for the ad UI.\n   */\n  public AdsMediaSource(\n      MediaSource contentMediaSource,\n      MediaSourceFactory adMediaSourceFactory,\n      AdsLoader adsLoader,\n      AdsLoader.AdViewProvider adViewProvider) {\n    this.contentMediaSource = contentMediaSource;\n    this.adMediaSourceFactory = adMediaSourceFactory;\n    this.adsLoader = adsLoader;\n    this.adViewProvider = adViewProvider;\n    mainHandler = new Handler(Looper.getMainLooper());\n    maskingMediaPeriodByAdMediaSource = new HashMap<>();\n    period = new Timeline.Period();\n    adGroupMediaSources = new MediaSource[0][];\n    adGroupTimelines = new Timeline[0][];\n    adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return contentMediaSource.getTag();\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    super.prepareSourceInternal(mediaTransferListener);\n    ComponentListener componentListener = new ComponentListener();\n    this.componentListener = componentListener;\n    prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource);\n    mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider));\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState);\n    if (adPlaybackState.adGroupCount > 0 && id.isAd()) {\n      int adGroupIndex = id.adGroupIndex;\n      int adIndexInAdGroup = id.adIndexInAdGroup;\n      Uri adUri =\n          Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]);\n      if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {\n        int adCount = adIndexInAdGroup + 1;\n        adGroupMediaSources[adGroupIndex] =\n            Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);\n        adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount);\n      }\n      MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];\n      if (mediaSource == null) {\n        mediaSource = adMediaSourceFactory.createMediaSource(adUri);\n        adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource;\n        maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>());\n        prepareChildSource(id, mediaSource);\n      }\n      MaskingMediaPeriod maskingMediaPeriod =\n          new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);\n      maskingMediaPeriod.setPrepareErrorListener(\n          new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));\n      List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource);\n      if (mediaPeriods == null) {\n        Object periodUid =\n            Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup])\n                .getUidOfPeriod(/* periodIndex= */ 0);\n        MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);\n        maskingMediaPeriod.createPeriod(adSourceMediaPeriodId);\n      } else {\n        // Keep track of the masking media period so it can be populated with the real media period\n        // when the source's info becomes available.\n        mediaPeriods.add(maskingMediaPeriod);\n      }\n      return maskingMediaPeriod;\n    } else {\n      MaskingMediaPeriod mediaPeriod =\n          new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs);\n      mediaPeriod.createPeriod(id);\n      return mediaPeriod;\n    }\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod;\n    List<MaskingMediaPeriod> mediaPeriods =\n        maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource);\n    if (mediaPeriods != null) {\n      mediaPeriods.remove(maskingMediaPeriod);\n    }\n    maskingMediaPeriod.releasePeriod();\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    Assertions.checkNotNull(componentListener).release();\n    componentListener = null;\n    maskingMediaPeriodByAdMediaSource.clear();\n    contentTimeline = null;\n    adPlaybackState = null;\n    adGroupMediaSources = new MediaSource[0][];\n    adGroupTimelines = new Timeline[0][];\n    mainHandler.post(adsLoader::stop);\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) {\n    if (mediaPeriodId.isAd()) {\n      int adGroupIndex = mediaPeriodId.adGroupIndex;\n      int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;\n      onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline);\n    } else {\n      onContentSourceInfoRefreshed(timeline);\n    }\n  }\n\n  @Override\n  protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      MediaPeriodId childId, MediaPeriodId mediaPeriodId) {\n    // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need\n    // to forward the reported mediaPeriodId in this case.\n    return childId.isAd() ? childId : mediaPeriodId;\n  }\n\n  // Internal methods.\n\n  private void onAdPlaybackState(AdPlaybackState adPlaybackState) {\n    if (this.adPlaybackState == null) {\n      adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];\n      Arrays.fill(adGroupMediaSources, new MediaSource[0]);\n      adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][];\n      Arrays.fill(adGroupTimelines, new Timeline[0]);\n    }\n    this.adPlaybackState = adPlaybackState;\n    maybeUpdateSourceInfo();\n  }\n\n  private void onContentSourceInfoRefreshed(Timeline timeline) {\n    Assertions.checkArgument(timeline.getPeriodCount() == 1);\n    contentTimeline = timeline;\n    maybeUpdateSourceInfo();\n  }\n\n  private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,\n      int adIndexInAdGroup, Timeline timeline) {\n    Assertions.checkArgument(timeline.getPeriodCount() == 1);\n    adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline;\n    List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource);\n    if (mediaPeriods != null) {\n      Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);\n      for (int i = 0; i < mediaPeriods.size(); i++) {\n        MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i);\n        MediaPeriodId adSourceMediaPeriodId =\n            new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber);\n        mediaPeriod.createPeriod(adSourceMediaPeriodId);\n      }\n    }\n    maybeUpdateSourceInfo();\n  }\n\n  private void maybeUpdateSourceInfo() {\n    Timeline contentTimeline = this.contentTimeline;\n    if (adPlaybackState != null && contentTimeline != null) {\n      adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period));\n      Timeline timeline =\n          adPlaybackState.adGroupCount == 0\n              ? contentTimeline\n              : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);\n      refreshSourceInfo(timeline);\n    }\n  }\n\n  private static long[][] getAdDurations(\n      @NullableType Timeline[][] adTimelines, Timeline.Period period) {\n    long[][] adDurations = new long[adTimelines.length][];\n    for (int i = 0; i < adTimelines.length; i++) {\n      adDurations[i] = new long[adTimelines[i].length];\n      for (int j = 0; j < adTimelines[i].length; j++) {\n        adDurations[i][j] =\n            adTimelines[i][j] == null\n                ? C.TIME_UNSET\n                : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs();\n      }\n    }\n    return adDurations;\n  }\n\n  /** Listener for component events. All methods are called on the main thread. */\n  private final class ComponentListener implements AdsLoader.EventListener {\n\n    private final Handler playerHandler;\n\n    private volatile boolean released;\n\n    /**\n     * Creates new listener which forwards ad playback states on the creating thread and all other\n     * events on the external event listener thread.\n     */\n    public ComponentListener() {\n      playerHandler = new Handler();\n    }\n\n    /** Releases the component listener. */\n    public void release() {\n      released = true;\n      playerHandler.removeCallbacksAndMessages(null);\n    }\n\n    @Override\n    public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {\n      if (released) {\n        return;\n      }\n      playerHandler.post(\n          () -> {\n            if (released) {\n              return;\n            }\n            AdsMediaSource.this.onAdPlaybackState(adPlaybackState);\n          });\n    }\n\n    @Override\n    public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) {\n      if (released) {\n        return;\n      }\n      createEventDispatcher(/* mediaPeriodId= */ null)\n          .loadError(\n              dataSpec,\n              dataSpec.uri,\n              /* responseHeaders= */ Collections.emptyMap(),\n              C.DATA_TYPE_AD,\n              C.TRACK_TYPE_UNKNOWN,\n              /* loadDurationMs= */ 0,\n              /* bytesLoaded= */ 0,\n              error,\n              /* wasCanceled= */ true);\n    }\n  }\n\n  private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener {\n\n    private final Uri adUri;\n    private final int adGroupIndex;\n    private final int adIndexInAdGroup;\n\n    public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) {\n      this.adUri = adUri;\n      this.adGroupIndex = adGroupIndex;\n      this.adIndexInAdGroup = adIndexInAdGroup;\n    }\n\n    @Override\n    public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) {\n      createEventDispatcher(mediaPeriodId)\n          .loadError(\n              new DataSpec(adUri),\n              adUri,\n              /* responseHeaders= */ Collections.emptyMap(),\n              C.DATA_TYPE_AD,\n              C.TRACK_TYPE_UNKNOWN,\n              /* loadDurationMs= */ 0,\n              /* bytesLoaded= */ 0,\n              AdLoadException.createForAd(exception),\n              /* wasCanceled= */ true);\n      mainHandler.post(\n          () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception));\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.ads;\n\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.ForwardingTimeline;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/** A {@link Timeline} for sources that have ads. */\n@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)\npublic final class SinglePeriodAdTimeline extends ForwardingTimeline {\n\n  private final AdPlaybackState adPlaybackState;\n\n  /**\n   * Creates a new timeline with a single period containing ads.\n   *\n   * @param contentTimeline The timeline of the content alongside which ads will be played. It must\n   *     have one window and one period.\n   * @param adPlaybackState The state of the period's ads.\n   */\n  public SinglePeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) {\n    super(contentTimeline);\n    Assertions.checkState(contentTimeline.getPeriodCount() == 1);\n    Assertions.checkState(contentTimeline.getWindowCount() == 1);\n    this.adPlaybackState = adPlaybackState;\n  }\n\n  @Override\n  public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    timeline.getPeriod(periodIndex, period, setIds);\n    period.set(\n        period.id,\n        period.uid,\n        period.windowIndex,\n        period.durationUs,\n        period.getPositionInWindowUs(),\n        adPlaybackState);\n    return period;\n  }\n\n  @Override\n  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n    window = super.getWindow(windowIndex, window, defaultPositionProjectionUs);\n    if (window.durationUs == C.TIME_UNSET) {\n      window.durationUs = adPlaybackState.contentDurationUs;\n    }\n    return window;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\n\n/**\n * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}.\n */\npublic abstract class BaseMediaChunk extends MediaChunk {\n\n  /**\n   * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the\n   * start of the chunk.\n   */\n  public final long clippedStartTimeUs;\n  /**\n   * The time from which output will end, or {@link C#TIME_UNSET} if output will end at the end of\n   * the chunk.\n   */\n  public final long clippedEndTimeUs;\n\n  private BaseMediaChunkOutput output;\n  private int[] firstSampleIndices;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.\n   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.\n   * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link\n   *     C#TIME_UNSET} to output from the start of the chunk.\n   * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link\n   *     C#TIME_UNSET} to output to the end of the chunk.\n   * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.\n   */\n  public BaseMediaChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      Format trackFormat,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs,\n      long clippedStartTimeUs,\n      long clippedEndTimeUs,\n      long chunkIndex) {\n    super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,\n        endTimeUs, chunkIndex);\n    this.clippedStartTimeUs = clippedStartTimeUs;\n    this.clippedEndTimeUs = clippedEndTimeUs;\n  }\n\n  /**\n   * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive\n   * samples as they are loaded.\n   *\n   * @param output The output that will receive the loaded media samples.\n   */\n  public void init(BaseMediaChunkOutput output) {\n    this.output = output;\n    firstSampleIndices = output.getWriteIndices();\n  }\n\n  /**\n   * Returns the index of the first sample in the specified track of the output that will originate\n   * from this chunk.\n   */\n  public final int getFirstSampleIndex(int trackIndex) {\n    return firstSampleIndices[trackIndex];\n  }\n\n  /**\n   * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}.\n   */\n  protected final BaseMediaChunkOutput getOutput() {\n    return output;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport java.util.NoSuchElementException;\n\n/**\n * Base class for {@link MediaChunkIterator}s. Handles {@link #next()} and {@link #isEnded()}, and\n * provides a bounds check for child classes.\n */\npublic abstract class BaseMediaChunkIterator implements MediaChunkIterator {\n\n  private final long fromIndex;\n  private final long toIndex;\n\n  private long currentIndex;\n\n  /**\n   * Creates base iterator.\n   *\n   * @param fromIndex The first available index.\n   * @param toIndex The last available index.\n   */\n  @SuppressWarnings(\"method.invocation.invalid\")\n  public BaseMediaChunkIterator(long fromIndex, long toIndex) {\n    this.fromIndex = fromIndex;\n    this.toIndex = toIndex;\n    reset();\n  }\n\n  @Override\n  public boolean isEnded() {\n    return currentIndex > toIndex;\n  }\n\n  @Override\n  public boolean next() {\n    currentIndex++;\n    return !isEnded();\n  }\n\n  @Override\n  public void reset() {\n    currentIndex = fromIndex - 1;\n  }\n\n  /**\n   * Verifies that the iterator points to a valid element.\n   *\n   * @throws NoSuchElementException If the iterator does not point to a valid element.\n   */\n  protected final void checkInBounds() {\n    if (currentIndex < fromIndex || currentIndex > toIndex) {\n      throw new NoSuchElementException();\n    }\n  }\n\n  /** Returns the current index this iterator is pointing to. */\n  protected final long getCurrentIndex() {\n    return currentIndex;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.extractor.DummyTrackOutput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.source.SampleQueue;\nimport com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;\nimport com.google.android.exoplayer2.util.Log;\n\n/** An output for {@link BaseMediaChunk}s. */\npublic final class BaseMediaChunkOutput implements TrackOutputProvider {\n\n  private static final String TAG = \"BaseMediaChunkOutput\";\n\n  private final int[] trackTypes;\n  private final SampleQueue[] sampleQueues;\n\n  /**\n   * @param trackTypes The track types of the individual track outputs.\n   * @param sampleQueues The individual sample queues.\n   */\n  public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) {\n    this.trackTypes = trackTypes;\n    this.sampleQueues = sampleQueues;\n  }\n\n  @Override\n  public TrackOutput track(int id, int type) {\n    for (int i = 0; i < trackTypes.length; i++) {\n      if (type == trackTypes[i]) {\n        return sampleQueues[i];\n      }\n    }\n    Log.e(TAG, \"Unmatched track of type: \" + type);\n    return new DummyTrackOutput();\n  }\n\n  /**\n   * Returns the current absolute write indices of the individual sample queues.\n   */\n  public int[] getWriteIndices() {\n    int[] writeIndices = new int[sampleQueues.length];\n    for (int i = 0; i < sampleQueues.length; i++) {\n      if (sampleQueues[i] != null) {\n        writeIndices[i] = sampleQueues[i].getWriteIndex();\n      }\n    }\n    return writeIndices;\n  }\n\n  /**\n   * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples\n   * subsequently written to the sample queues.\n   */\n  public void setSampleOffsetUs(long sampleOffsetUs) {\n    for (SampleQueue sampleQueue : sampleQueues) {\n      if (sampleQueue != null) {\n        sampleQueue.setSampleOffsetUs(sampleOffsetUs);\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport com.google.android.exoplayer2.upstream.StatsDataSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * An abstract base class for {@link Loadable} implementations that load chunks of data required\n * for the playback of streams.\n */\npublic abstract class Chunk implements Loadable {\n\n  /**\n   * The {@link DataSpec} that defines the data to be loaded.\n   */\n  public final DataSpec dataSpec;\n  /**\n   * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For\n   * reporting only.\n   */\n  public final int type;\n  /**\n   * The format of the track to which this chunk belongs, or null if the chunk does not belong to\n   * a track.\n   */\n  public final Format trackFormat;\n  /**\n   * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track.\n   * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track.\n   */\n  public final int trackSelectionReason;\n  /**\n   * Optional data associated with the selection of the track to which this chunk belongs. Null if\n   * the chunk does not belong to a track.\n   */\n  @Nullable public final Object trackSelectionData;\n  /**\n   * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data\n   * being loaded does not contain media samples.\n   */\n  public final long startTimeUs;\n  /**\n   * The end time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data being\n   * loaded does not contain media samples.\n   */\n  public final long endTimeUs;\n\n  protected final StatsDataSource dataSource;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param type See {@link #type}.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param startTimeUs See {@link #startTimeUs}.\n   * @param endTimeUs See {@link #endTimeUs}.\n   */\n  public Chunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      int type,\n      Format trackFormat,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs) {\n    this.dataSource = new StatsDataSource(dataSource);\n    this.dataSpec = Assertions.checkNotNull(dataSpec);\n    this.type = type;\n    this.trackFormat = trackFormat;\n    this.trackSelectionReason = trackSelectionReason;\n    this.trackSelectionData = trackSelectionData;\n    this.startTimeUs = startTimeUs;\n    this.endTimeUs = endTimeUs;\n  }\n\n  /**\n   * Returns the duration of the chunk in microseconds.\n   */\n  public final long getDurationUs() {\n    return endTimeUs - startTimeUs;\n  }\n\n  /**\n   * Returns the number of bytes that have been loaded. Must only be called after the load\n   * completed, failed, or was canceled.\n   */\n  public final long bytesLoaded() {\n    return dataSource.getBytesRead();\n  }\n\n  /**\n   * Returns the {@link Uri} associated with the last {@link DataSource#open} call. If redirection\n   * occurred, this is the redirected uri. Must only be called after the load completed, failed, or\n   * was canceled.\n   *\n   * @see DataSource#getUri()\n   */\n  public final Uri getUri() {\n    return dataSource.getLastOpenedUri();\n  }\n\n  /**\n   * Returns the response headers associated with the last {@link DataSource#open} call. Must only\n   * be called after the load completed, failed, or was canceled.\n   *\n   * @see DataSource#getResponseHeaders()\n   */\n  public final Map<String, List<String>> getResponseHeaders() {\n    return dataSource.getLastResponseHeaders();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport android.util.SparseArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.DummyTrackOutput;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\n\n/**\n * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly\n * additional embedded tracks.\n * <p>\n * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data.\n */\npublic final class ChunkExtractorWrapper implements ExtractorOutput {\n\n  /**\n   * Provides {@link TrackOutput} instances to be written to by the wrapper.\n   */\n  public interface TrackOutputProvider {\n\n    /**\n     * Called to get the {@link TrackOutput} for a specific track.\n     * <p>\n     * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.\n     *\n     * @param id A track identifier.\n     * @param type The type of the track. Typically one of the\n     *     {@link C} {@code TRACK_TYPE_*} constants.\n     * @return The {@link TrackOutput} for the given track identifier.\n     */\n    TrackOutput track(int id, int type);\n\n  }\n\n  public final Extractor extractor;\n\n  private final int primaryTrackType;\n  private final Format primaryTrackManifestFormat;\n  private final SparseArray<BindingTrackOutput> bindingTrackOutputs;\n\n  private boolean extractorInitialized;\n  private TrackOutputProvider trackOutputProvider;\n  private long endTimeUs;\n  private SeekMap seekMap;\n  private Format[] sampleFormats;\n\n  /**\n   * @param extractor The extractor to wrap.\n   * @param primaryTrackType The type of the primary track. Typically one of the\n   *     {@link C} {@code TRACK_TYPE_*} constants.\n   * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged\n   *     into any sample {@link Format} output from the {@link Extractor} for the primary track.\n   */\n  public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType,\n      Format primaryTrackManifestFormat) {\n    this.extractor = extractor;\n    this.primaryTrackType = primaryTrackType;\n    this.primaryTrackManifestFormat = primaryTrackManifestFormat;\n    bindingTrackOutputs = new SparseArray<>();\n  }\n\n  /**\n   * Returns the {@link SeekMap} most recently output by the extractor, or null.\n   */\n  public SeekMap getSeekMap() {\n    return seekMap;\n  }\n\n  /**\n   * Returns the sample {@link Format}s most recently output by the extractor, or null.\n   */\n  public Format[] getSampleFormats() {\n    return sampleFormats;\n  }\n\n  /**\n   * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link\n   * TrackOutputProvider}, and configures the extractor to receive data from a new chunk.\n   *\n   * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data.\n   * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output\n   *     samples from the start of the chunk.\n   * @param endTimeUs The end position in the new chunk, or {@link C#TIME_UNSET} to output samples\n   *     to the end of the chunk.\n   */\n  public void init(\n      @Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) {\n    this.trackOutputProvider = trackOutputProvider;\n    this.endTimeUs = endTimeUs;\n    if (!extractorInitialized) {\n      extractor.init(this);\n      if (startTimeUs != C.TIME_UNSET) {\n        extractor.seek(/* position= */ 0, startTimeUs);\n      }\n      extractorInitialized = true;\n    } else {\n      extractor.seek(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs);\n      for (int i = 0; i < bindingTrackOutputs.size(); i++) {\n        bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs);\n      }\n    }\n  }\n\n  // ExtractorOutput implementation.\n\n  @Override\n  public TrackOutput track(int id, int type) {\n    BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id);\n    if (bindingTrackOutput == null) {\n      // Assert that if we're seeing a new track we have not seen endTracks.\n      Assertions.checkState(sampleFormats == null);\n      // TODO: Manifest formats for embedded tracks should also be passed here.\n      bindingTrackOutput = new BindingTrackOutput(id, type,\n          type == primaryTrackType ? primaryTrackManifestFormat : null);\n      bindingTrackOutput.bind(trackOutputProvider, endTimeUs);\n      bindingTrackOutputs.put(id, bindingTrackOutput);\n    }\n    return bindingTrackOutput;\n  }\n\n  @Override\n  public void endTracks() {\n    Format[] sampleFormats = new Format[bindingTrackOutputs.size()];\n    for (int i = 0; i < bindingTrackOutputs.size(); i++) {\n      sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat;\n    }\n    this.sampleFormats = sampleFormats;\n  }\n\n  @Override\n  public void seekMap(SeekMap seekMap) {\n    this.seekMap = seekMap;\n  }\n\n  // Internal logic.\n\n  private static final class BindingTrackOutput implements TrackOutput {\n\n    private final int id;\n    private final int type;\n    private final Format manifestFormat;\n    private final DummyTrackOutput dummyTrackOutput;\n\n    public Format sampleFormat;\n    private TrackOutput trackOutput;\n    private long endTimeUs;\n\n    public BindingTrackOutput(int id, int type, Format manifestFormat) {\n      this.id = id;\n      this.type = type;\n      this.manifestFormat = manifestFormat;\n      dummyTrackOutput = new DummyTrackOutput();\n    }\n\n    public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) {\n      if (trackOutputProvider == null) {\n        trackOutput = dummyTrackOutput;\n        return;\n      }\n      this.endTimeUs = endTimeUs;\n      trackOutput = trackOutputProvider.track(id, type);\n      if (sampleFormat != null) {\n        trackOutput.format(sampleFormat);\n      }\n    }\n\n    @Override\n    public void format(Format format) {\n      sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat)\n          : format;\n      trackOutput.format(sampleFormat);\n    }\n\n    @Override\n    public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n        throws IOException, InterruptedException {\n      return trackOutput.sampleData(input, length, allowEndOfInput);\n    }\n\n    @Override\n    public void sampleData(ParsableByteArray data, int length) {\n      trackOutput.sampleData(data, length);\n    }\n\n    @Override\n    public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,\n        CryptoData cryptoData) {\n      if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) {\n        trackOutput = dummyTrackOutput;\n      }\n      trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Holds a chunk or an indication that the end of the stream has been reached.\n */\npublic final class ChunkHolder {\n\n  /** The chunk. */\n  @Nullable public Chunk chunk;\n\n  /**\n   * Indicates that the end of the stream has been reached.\n   */\n  public boolean endOfStream;\n\n  /**\n   * Clears the holder.\n   */\n  public void clear() {\n    chunk = null;\n    endOfStream = false;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleQueue;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.\n * May also be configured to expose additional embedded {@link SampleStream}s.\n */\npublic class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,\n    Loader.Callback<Chunk>, Loader.ReleaseCallback {\n\n  /** A callback to be notified when a sample stream has finished being released. */\n  public interface ReleaseCallback<T extends ChunkSource> {\n\n    /**\n     * Called when the {@link ChunkSampleStream} has finished being released.\n     *\n     * @param chunkSampleStream The released sample stream.\n     */\n    void onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream);\n  }\n\n  private static final String TAG = \"ChunkSampleStream\";\n\n  public final int primaryTrackType;\n\n  @Nullable private final int[] embeddedTrackTypes;\n  @Nullable private final Format[] embeddedTrackFormats;\n  private final boolean[] embeddedTracksSelected;\n  private final T chunkSource;\n  private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback;\n  private final EventDispatcher eventDispatcher;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final Loader loader;\n  private final ChunkHolder nextChunkHolder;\n  private final ArrayList<BaseMediaChunk> mediaChunks;\n  private final List<BaseMediaChunk> readOnlyMediaChunks;\n  private final SampleQueue primarySampleQueue;\n  private final SampleQueue[] embeddedSampleQueues;\n  private final BaseMediaChunkOutput mediaChunkOutput;\n\n  private Format primaryDownstreamTrackFormat;\n  @Nullable private ReleaseCallback<T> releaseCallback;\n  private long pendingResetPositionUs;\n  private long lastSeekPositionUs;\n  private int nextNotifyPrimaryFormatMediaChunkIndex;\n\n  /* package */ long decodeOnlyUntilPositionUs;\n  /* package */ boolean loadingFinished;\n\n  /**\n   * Constructs an instance.\n   *\n   * @param primaryTrackType The type of the primary track. One of the {@link C} {@code\n   *     TRACK_TYPE_*} constants.\n   * @param embeddedTrackTypes The types of any embedded tracks, or null.\n   * @param embeddedTrackFormats The formats of the embedded tracks, or null.\n   * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.\n   * @param callback An {@link Callback} for the stream.\n   * @param allocator An {@link Allocator} from which allocations can be obtained.\n   * @param positionUs The position from which to start loading media.\n   * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}\n   *     from.\n   * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.\n   * @param eventDispatcher A dispatcher to notify of events.\n   */\n  public ChunkSampleStream(\n      int primaryTrackType,\n      @Nullable int[] embeddedTrackTypes,\n      @Nullable Format[] embeddedTrackFormats,\n      T chunkSource,\n      Callback<ChunkSampleStream<T>> callback,\n      Allocator allocator,\n      long positionUs,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher) {\n    this.primaryTrackType = primaryTrackType;\n    this.embeddedTrackTypes = embeddedTrackTypes;\n    this.embeddedTrackFormats = embeddedTrackFormats;\n    this.chunkSource = chunkSource;\n    this.callback = callback;\n    this.eventDispatcher = eventDispatcher;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    loader = new Loader(\"Loader:ChunkSampleStream\");\n    nextChunkHolder = new ChunkHolder();\n    mediaChunks = new ArrayList<>();\n    readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);\n\n    int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;\n    embeddedSampleQueues = new SampleQueue[embeddedTrackCount];\n    embeddedTracksSelected = new boolean[embeddedTrackCount];\n    int[] trackTypes = new int[1 + embeddedTrackCount];\n    SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];\n\n    primarySampleQueue = new SampleQueue(allocator, drmSessionManager);\n    trackTypes[0] = primaryTrackType;\n    sampleQueues[0] = primarySampleQueue;\n\n    for (int i = 0; i < embeddedTrackCount; i++) {\n      SampleQueue sampleQueue =\n          new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager());\n      embeddedSampleQueues[i] = sampleQueue;\n      sampleQueues[i + 1] = sampleQueue;\n      trackTypes[i + 1] = embeddedTrackTypes[i];\n    }\n\n    mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues);\n    pendingResetPositionUs = positionUs;\n    lastSeekPositionUs = positionUs;\n  }\n\n  /**\n   * Discards buffered media up to the specified position.\n   *\n   * @param positionUs The position to discard up to, in microseconds.\n   * @param toKeyframe If true then for each track discards samples up to the keyframe before or at\n   *     the specified position, rather than any sample before or at that position.\n   */\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    if (isPendingReset()) {\n      return;\n    }\n    int oldFirstSampleIndex = primarySampleQueue.getFirstIndex();\n    primarySampleQueue.discardTo(positionUs, toKeyframe, true);\n    int newFirstSampleIndex = primarySampleQueue.getFirstIndex();\n    if (newFirstSampleIndex > oldFirstSampleIndex) {\n      long discardToUs = primarySampleQueue.getFirstTimestampUs();\n      for (int i = 0; i < embeddedSampleQueues.length; i++) {\n        embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]);\n      }\n    }\n    discardDownstreamMediaChunks(newFirstSampleIndex);\n  }\n\n  /**\n   * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's\n   * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned\n   * stream when the track is no longer required, and before calling this method again to obtain\n   * another stream for the same track.\n   *\n   * @param positionUs The current playback position in microseconds.\n   * @param trackType The type of the embedded track to enable.\n   * @return The {@link EmbeddedSampleStream} for the embedded track.\n   */\n  public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) {\n    for (int i = 0; i < embeddedSampleQueues.length; i++) {\n      if (embeddedTrackTypes[i] == trackType) {\n        Assertions.checkState(!embeddedTracksSelected[i]);\n        embeddedTracksSelected[i] = true;\n        embeddedSampleQueues[i].rewind();\n        embeddedSampleQueues[i].advanceTo(positionUs, true, true);\n        return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);\n      }\n    }\n    // Should never happen.\n    throw new IllegalStateException();\n  }\n\n  /**\n   * Returns the {@link ChunkSource} used by this stream.\n   */\n  public T getChunkSource() {\n    return chunkSource;\n  }\n\n  /**\n   * Returns an estimate of the position up to which data is buffered.\n   *\n   * @return An estimate of the absolute position in microseconds up to which data is buffered, or\n   *     {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.\n   */\n  @Override\n  public long getBufferedPositionUs() {\n    if (loadingFinished) {\n      return C.TIME_END_OF_SOURCE;\n    } else if (isPendingReset()) {\n      return pendingResetPositionUs;\n    } else {\n      long bufferedPositionUs = lastSeekPositionUs;\n      BaseMediaChunk lastMediaChunk = getLastMediaChunk();\n      BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk\n          : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;\n      if (lastCompletedMediaChunk != null) {\n        bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);\n      }\n      return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs());\n    }\n  }\n\n  /**\n   * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used\n   * as sync points.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @param seekParameters Parameters that control how the seek is performed.\n   * @return The adjusted seek position, in microseconds.\n   */\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);\n  }\n\n  /**\n   * Seeks to the specified position in microseconds.\n   *\n   * @param positionUs The seek position in microseconds.\n   */\n  public void seekToUs(long positionUs) {\n    lastSeekPositionUs = positionUs;\n    if (isPendingReset()) {\n      // A reset is already pending. We only need to update its position.\n      pendingResetPositionUs = positionUs;\n      return;\n    }\n\n    // Detect whether the seek is to the start of a chunk that's at least partially buffered.\n    BaseMediaChunk seekToMediaChunk = null;\n    for (int i = 0; i < mediaChunks.size(); i++) {\n      BaseMediaChunk mediaChunk = mediaChunks.get(i);\n      long mediaChunkStartTimeUs = mediaChunk.startTimeUs;\n      if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) {\n        seekToMediaChunk = mediaChunk;\n        break;\n      } else if (mediaChunkStartTimeUs > positionUs) {\n        // We're not going to find a chunk with a matching start time.\n        break;\n      }\n    }\n\n    // See if we can seek inside the primary sample queue.\n    boolean seekInsideBuffer;\n    primarySampleQueue.rewind();\n    if (seekToMediaChunk != null) {\n      // When seeking to the start of a chunk we use the index of the first sample in the chunk\n      // rather than the seek position. This ensures we seek to the keyframe at the start of the\n      // chunk even if the sample timestamps are slightly offset from the chunk start times.\n      seekInsideBuffer =\n          primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0));\n      decodeOnlyUntilPositionUs = 0;\n    } else {\n      seekInsideBuffer =\n          primarySampleQueue.advanceTo(\n                  positionUs,\n                  /* toKeyframe= */ true,\n                  /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs())\n              != SampleQueue.ADVANCE_FAILED;\n      decodeOnlyUntilPositionUs = lastSeekPositionUs;\n    }\n\n    if (seekInsideBuffer) {\n      // We can seek inside the buffer.\n      nextNotifyPrimaryFormatMediaChunkIndex =\n          primarySampleIndexToMediaChunkIndex(\n              primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0);\n      // Advance the embedded sample queues to the seek position.\n      for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n        embeddedSampleQueue.rewind();\n        embeddedSampleQueue.advanceTo(positionUs, true, false);\n      }\n    } else {\n      // We can't seek inside the buffer, and so need to reset.\n      pendingResetPositionUs = positionUs;\n      loadingFinished = false;\n      mediaChunks.clear();\n      nextNotifyPrimaryFormatMediaChunkIndex = 0;\n      if (loader.isLoading()) {\n        loader.cancelLoading();\n      } else {\n        loader.clearFatalError();\n        primarySampleQueue.reset();\n        for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n          embeddedSampleQueue.reset();\n        }\n      }\n    }\n  }\n\n  /**\n   * Releases the stream.\n   *\n   * <p>This method should be called when the stream is no longer required. Either this method or\n   * {@link #release(ReleaseCallback)} can be used to release this stream.\n   */\n  public void release() {\n    release(null);\n  }\n\n  /**\n   * Releases the stream.\n   *\n   * <p>This method should be called when the stream is no longer required. Either this method or\n   * {@link #release()} can be used to release this stream.\n   *\n   * @param callback An optional callback to be called on the loading thread once the loader has\n   *     been released.\n   */\n  public void release(@Nullable ReleaseCallback<T> callback) {\n    this.releaseCallback = callback;\n    // Discard as much as we can synchronously.\n    primarySampleQueue.preRelease();\n    for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n      embeddedSampleQueue.preRelease();\n    }\n    loader.release(this);\n  }\n\n  @Override\n  public void onLoaderReleased() {\n    primarySampleQueue.release();\n    for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n      embeddedSampleQueue.release();\n    }\n    if (releaseCallback != null) {\n      releaseCallback.onSampleStreamReleased(this);\n    }\n  }\n\n  // SampleStream implementation.\n\n  @Override\n  public boolean isReady() {\n    return !isPendingReset() && primarySampleQueue.isReady(loadingFinished);\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    loader.maybeThrowError();\n    primarySampleQueue.maybeThrowError();\n    if (!loader.isLoading()) {\n      chunkSource.maybeThrowError();\n    }\n  }\n\n  @Override\n  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n      boolean formatRequired) {\n    if (isPendingReset()) {\n      return C.RESULT_NOTHING_READ;\n    }\n    maybeNotifyPrimaryTrackFormatChanged();\n\n    return primarySampleQueue.read(\n        formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);\n  }\n\n  @Override\n  public int skipData(long positionUs) {\n    if (isPendingReset()) {\n      return 0;\n    }\n    int skipCount;\n    if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {\n      skipCount = primarySampleQueue.advanceToEnd();\n    } else {\n      skipCount = primarySampleQueue.advanceTo(positionUs, true, true);\n      if (skipCount == SampleQueue.ADVANCE_FAILED) {\n        skipCount = 0;\n      }\n    }\n    maybeNotifyPrimaryTrackFormatChanged();\n    return skipCount;\n  }\n\n  // Loader.Callback implementation.\n\n  @Override\n  public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {\n    chunkSource.onChunkLoadCompleted(loadable);\n    eventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        primaryTrackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    callback.onContinueLoadingRequested(this);\n  }\n\n  @Override\n  public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,\n      boolean released) {\n    eventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        primaryTrackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    if (!released) {\n      primarySampleQueue.reset();\n      for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n        embeddedSampleQueue.reset();\n      }\n      callback.onContinueLoadingRequested(this);\n    }\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      Chunk loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long bytesLoaded = loadable.bytesLoaded();\n    boolean isMediaChunk = isMediaChunk(loadable);\n    int lastChunkIndex = mediaChunks.size() - 1;\n    boolean cancelable =\n        bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex);\n    long blacklistDurationMs =\n        cancelable\n            ? loadErrorHandlingPolicy.getBlacklistDurationMsFor(\n                loadable.type, loadDurationMs, error, errorCount)\n            : C.TIME_UNSET;\n    LoadErrorAction loadErrorAction = null;\n    if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) {\n      if (cancelable) {\n        loadErrorAction = Loader.DONT_RETRY;\n        if (isMediaChunk) {\n          BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);\n          Assertions.checkState(removed == loadable);\n          if (mediaChunks.isEmpty()) {\n            pendingResetPositionUs = lastSeekPositionUs;\n          }\n        }\n      } else {\n        Log.w(TAG, \"Ignoring attempt to cancel non-cancelable load.\");\n      }\n    }\n\n    if (loadErrorAction == null) {\n      // The load was not cancelled. Either the load must be retried or the error propagated.\n      long retryDelayMs =\n          loadErrorHandlingPolicy.getRetryDelayMsFor(\n              loadable.type, loadDurationMs, error, errorCount);\n      loadErrorAction =\n          retryDelayMs != C.TIME_UNSET\n              ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs)\n              : Loader.DONT_RETRY_FATAL;\n    }\n\n    boolean canceled = !loadErrorAction.isRetry();\n    eventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        primaryTrackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        bytesLoaded,\n        error,\n        canceled);\n    if (canceled) {\n      callback.onContinueLoadingRequested(this);\n    }\n    return loadErrorAction;\n  }\n\n  // SequenceableLoader implementation\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {\n      return false;\n    }\n\n    boolean pendingReset = isPendingReset();\n    List<BaseMediaChunk> chunkQueue;\n    long loadPositionUs;\n    if (pendingReset) {\n      chunkQueue = Collections.emptyList();\n      loadPositionUs = pendingResetPositionUs;\n    } else {\n      chunkQueue = readOnlyMediaChunks;\n      loadPositionUs = getLastMediaChunk().endTimeUs;\n    }\n    chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder);\n    boolean endOfStream = nextChunkHolder.endOfStream;\n    Chunk loadable = nextChunkHolder.chunk;\n    nextChunkHolder.clear();\n\n    if (endOfStream) {\n      pendingResetPositionUs = C.TIME_UNSET;\n      loadingFinished = true;\n      return true;\n    }\n\n    if (loadable == null) {\n      return false;\n    }\n\n    if (isMediaChunk(loadable)) {\n      BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;\n      if (pendingReset) {\n        boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs;\n        // Only enable setting of the decode only flag if we're not resetting to a chunk boundary.\n        decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs;\n        pendingResetPositionUs = C.TIME_UNSET;\n      }\n      mediaChunk.init(mediaChunkOutput);\n      mediaChunks.add(mediaChunk);\n    }\n    long elapsedRealtimeMs =\n        loader.startLoading(\n            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));\n    eventDispatcher.loadStarted(\n        loadable.dataSpec,\n        loadable.type,\n        primaryTrackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs);\n    return true;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return loader.isLoading();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    if (isPendingReset()) {\n      return pendingResetPositionUs;\n    } else {\n      return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) {\n      return;\n    }\n\n    int currentQueueSize = mediaChunks.size();\n    int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);\n    if (currentQueueSize <= preferredQueueSize) {\n      return;\n    }\n\n    int newQueueSize = currentQueueSize;\n    for (int i = preferredQueueSize; i < currentQueueSize; i++) {\n      if (!haveReadFromMediaChunk(i)) {\n        newQueueSize = i;\n        break;\n      }\n    }\n    if (newQueueSize == currentQueueSize) {\n      return;\n    }\n\n    long endTimeUs = getLastMediaChunk().endTimeUs;\n    BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize);\n    if (mediaChunks.isEmpty()) {\n      pendingResetPositionUs = lastSeekPositionUs;\n    }\n    loadingFinished = false;\n    eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs);\n  }\n\n  // Internal methods\n\n  private boolean isMediaChunk(Chunk chunk) {\n    return chunk instanceof BaseMediaChunk;\n  }\n\n  /** Returns whether samples have been read from media chunk at given index. */\n  private boolean haveReadFromMediaChunk(int mediaChunkIndex) {\n    BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);\n    if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {\n      return true;\n    }\n    for (int i = 0; i < embeddedSampleQueues.length; i++) {\n      if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /* package */ boolean isPendingReset() {\n    return pendingResetPositionUs != C.TIME_UNSET;\n  }\n\n  private void discardDownstreamMediaChunks(int discardToSampleIndex) {\n    int discardToMediaChunkIndex =\n        primarySampleIndexToMediaChunkIndex(discardToSampleIndex, /* minChunkIndex= */ 0);\n    // Don't discard any chunks that we haven't reported the primary format change for yet.\n    discardToMediaChunkIndex =\n        Math.min(discardToMediaChunkIndex, nextNotifyPrimaryFormatMediaChunkIndex);\n    if (discardToMediaChunkIndex > 0) {\n      Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex);\n      nextNotifyPrimaryFormatMediaChunkIndex -= discardToMediaChunkIndex;\n    }\n  }\n\n  private void maybeNotifyPrimaryTrackFormatChanged() {\n    int readSampleIndex = primarySampleQueue.getReadIndex();\n    int notifyToMediaChunkIndex =\n        primarySampleIndexToMediaChunkIndex(\n            readSampleIndex, /* minChunkIndex= */ nextNotifyPrimaryFormatMediaChunkIndex - 1);\n    while (nextNotifyPrimaryFormatMediaChunkIndex <= notifyToMediaChunkIndex) {\n      maybeNotifyPrimaryTrackFormatChanged(nextNotifyPrimaryFormatMediaChunkIndex++);\n    }\n  }\n\n  private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) {\n    BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex);\n    Format trackFormat = currentChunk.trackFormat;\n    if (!trackFormat.equals(primaryDownstreamTrackFormat)) {\n      eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,\n          currentChunk.trackSelectionReason, currentChunk.trackSelectionData,\n          currentChunk.startTimeUs);\n    }\n    primaryDownstreamTrackFormat = trackFormat;\n  }\n\n  /**\n   * Returns the media chunk index corresponding to a given primary sample index.\n   *\n   * @param primarySampleIndex The primary sample index for which the corresponding media chunk\n   *     index is required.\n   * @param minChunkIndex A minimum chunk index from which to start searching, or -1 if no hint can\n   *     be provided.\n   * @return The index of the media chunk corresponding to the sample index, or -1 if the list of\n   *     media chunks is empty, or {@code minChunkIndex} if the sample precedes the first chunk in\n   *     the search (i.e. the chunk at {@code minChunkIndex}, or at index 0 if {@code minChunkIndex}\n   *     is -1.\n   */\n  private int primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex) {\n    for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) {\n      if (mediaChunks.get(i).getFirstSampleIndex(0) > primarySampleIndex) {\n        return i - 1;\n      }\n    }\n    return mediaChunks.size() - 1;\n  }\n\n  private BaseMediaChunk getLastMediaChunk() {\n    return mediaChunks.get(mediaChunks.size() - 1);\n  }\n\n  /**\n   * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample\n   * queues.\n   *\n   * @param chunkIndex The index of the first chunk to discard.\n   * @return The chunk at given index.\n   */\n  private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {\n    BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);\n    Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());\n    nextNotifyPrimaryFormatMediaChunkIndex =\n        Math.max(nextNotifyPrimaryFormatMediaChunkIndex, mediaChunks.size());\n    primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));\n    for (int i = 0; i < embeddedSampleQueues.length; i++) {\n      embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));\n    }\n    return firstRemovedChunk;\n  }\n\n  /**\n   * A {@link SampleStream} embedded in a {@link ChunkSampleStream}.\n   */\n  public final class EmbeddedSampleStream implements SampleStream {\n\n    public final ChunkSampleStream<T> parent;\n\n    private final SampleQueue sampleQueue;\n    private final int index;\n\n    private boolean notifiedDownstreamFormat;\n\n    public EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index) {\n      this.parent = parent;\n      this.sampleQueue = sampleQueue;\n      this.index = index;\n    }\n\n    @Override\n    public boolean isReady() {\n      return !isPendingReset() && sampleQueue.isReady(loadingFinished);\n    }\n\n    @Override\n    public int skipData(long positionUs) {\n      if (isPendingReset()) {\n        return 0;\n      }\n      maybeNotifyDownstreamFormat();\n      int skipCount;\n      if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {\n        skipCount = sampleQueue.advanceToEnd();\n      } else {\n        skipCount = sampleQueue.advanceTo(positionUs, true, true);\n        if (skipCount == SampleQueue.ADVANCE_FAILED) {\n          skipCount = 0;\n        }\n      }\n      return skipCount;\n    }\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      // Do nothing. Errors will be thrown from the primary stream.\n    }\n\n    @Override\n    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n        boolean formatRequired) {\n      if (isPendingReset()) {\n        return C.RESULT_NOTHING_READ;\n      }\n      maybeNotifyDownstreamFormat();\n      return sampleQueue.read(\n          formatHolder,\n          buffer,\n          formatRequired,\n          loadingFinished,\n          decodeOnlyUntilPositionUs);\n    }\n\n    public void release() {\n      Assertions.checkState(embeddedTracksSelected[index]);\n      embeddedTracksSelected[index] = false;\n    }\n\n    private void maybeNotifyDownstreamFormat() {\n      if (!notifiedDownstreamFormat) {\n        eventDispatcher.downstreamFormatChanged(\n            embeddedTrackTypes[index],\n            embeddedTrackFormats[index],\n            C.SELECTION_REASON_UNKNOWN,\n            /* trackSelectionData= */ null,\n            lastSeekPositionUs);\n        notifiedDownstreamFormat = true;\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.SeekParameters;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load.\n */\npublic interface ChunkSource {\n\n  /**\n   * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used\n   * as sync points.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @param seekParameters Parameters that control how the seek is performed.\n   * @return The adjusted seek position, in microseconds.\n   */\n  long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);\n\n  /**\n   * If the source is currently having difficulty providing chunks, then this method throws the\n   * underlying error. Otherwise does nothing.\n   * <p>\n   * This method should only be called after the source has been prepared.\n   *\n   * @throws IOException The underlying error.\n   */\n  void maybeThrowError() throws IOException;\n\n  /**\n   * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.\n   * <p>\n   * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced\n   * with chunks of a significantly higher quality (e.g. because the available bandwidth has\n   * substantially increased).\n   *\n   * @param playbackPositionUs The current playback position.\n   * @param queue The queue of buffered {@link MediaChunk}s.\n   * @return The preferred queue size.\n   */\n  int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);\n\n  /**\n   * Returns the next chunk to load.\n   *\n   * <p>If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has\n   * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the\n   * end of the stream has not been reached, the {@link ChunkHolder} is not modified.\n   *\n   * @param playbackPositionUs The current playback position in microseconds. If playback of the\n   *     period to which this chunk source belongs has not yet started, the value will be the\n   *     starting position in the period minus the duration of any media in previous periods still\n   *     to be played.\n   * @param loadPositionUs The current load position in microseconds. If {@code queue} is empty,\n   *     this is the starting position from which chunks should be provided. Else it's equal to\n   *     {@link MediaChunk#endTimeUs} of the last chunk in the {@code queue}.\n   * @param queue The queue of buffered {@link MediaChunk}s.\n   * @param out A holder to populate.\n   */\n  void getNextChunk(\n          long playbackPositionUs,\n          long loadPositionUs,\n          List<? extends MediaChunk> queue,\n          ChunkHolder out);\n\n  /**\n   * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this\n   * source.\n   *\n   * <p>This method should only be called when the source is enabled.\n   *\n   * @param chunk The chunk whose load has been completed.\n   */\n  void onChunkLoadCompleted(Chunk chunk);\n\n  /**\n   * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from\n   * this source.\n   *\n   * <p>This method should only be called when the source is enabled.\n   *\n   * @param chunk The chunk whose load encountered the error.\n   * @param cancelable Whether the load can be canceled.\n   * @param e The error.\n   * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or\n   *     {@link C#TIME_UNSET} if the track may not be blacklisted.\n   * @return Whether the load should be canceled so that a replacement chunk can be loaded instead.\n   *     Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link\n   *     #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement\n   *     chunk.\n   */\n  boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorInput;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.\n */\npublic class ContainerMediaChunk extends BaseMediaChunk {\n\n  private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();\n\n  private final int chunkCount;\n  private final long sampleOffsetUs;\n  private final ChunkExtractorWrapper extractorWrapper;\n\n  private long nextLoadPosition;\n  private volatile boolean loadCanceled;\n  private boolean loadCompleted;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.\n   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.\n   * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link\n   *     C#TIME_UNSET} to output from the start of the chunk.\n   * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link\n   *     C#TIME_UNSET} to output to the end of the chunk.\n   * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.\n   * @param chunkCount The number of chunks in the underlying media that are spanned by this\n   *     instance. Normally equal to one, but may be larger if multiple chunks as defined by the\n   *     underlying media are being merged into a single load.\n   * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.\n   * @param extractorWrapper A wrapped extractor to use for parsing the data.\n   */\n  public ContainerMediaChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      Format trackFormat,\n      int trackSelectionReason,\n      Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs,\n      long clippedStartTimeUs,\n      long clippedEndTimeUs,\n      long chunkIndex,\n      int chunkCount,\n      long sampleOffsetUs,\n      ChunkExtractorWrapper extractorWrapper) {\n    super(\n        dataSource,\n        dataSpec,\n        trackFormat,\n        trackSelectionReason,\n        trackSelectionData,\n        startTimeUs,\n        endTimeUs,\n        clippedStartTimeUs,\n        clippedEndTimeUs,\n        chunkIndex);\n    this.chunkCount = chunkCount;\n    this.sampleOffsetUs = sampleOffsetUs;\n    this.extractorWrapper = extractorWrapper;\n  }\n\n  @Override\n  public long getNextChunkIndex() {\n    return chunkIndex + chunkCount;\n  }\n\n  @Override\n  public boolean isLoadCompleted() {\n    return loadCompleted;\n  }\n\n  // Loadable implementation.\n\n  @Override\n  public final void cancelLoad() {\n    loadCanceled = true;\n  }\n\n  @SuppressWarnings(\"NonAtomicVolatileUpdate\")\n  @Override\n  public final void load() throws IOException, InterruptedException {\n    DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);\n    try {\n      // Create and open the input.\n      ExtractorInput input = new DefaultExtractorInput(dataSource,\n          loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));\n      if (nextLoadPosition == 0) {\n        // Configure the output and set it as the target for the extractor wrapper.\n        BaseMediaChunkOutput output = getOutput();\n        output.setSampleOffsetUs(sampleOffsetUs);\n        extractorWrapper.init(\n            getTrackOutputProvider(output),\n            clippedStartTimeUs == C.TIME_UNSET\n                ? C.TIME_UNSET\n                : (clippedStartTimeUs - sampleOffsetUs),\n            clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs));\n      }\n      // Load and decode the sample data.\n      try {\n        Extractor extractor = extractorWrapper.extractor;\n        int result = Extractor.RESULT_CONTINUE;\n        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {\n          result = extractor.read(input, DUMMY_POSITION_HOLDER);\n        }\n        Assertions.checkState(result != Extractor.RESULT_SEEK);\n      } finally {\n        nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n    loadCompleted = true;\n  }\n\n  /**\n   * Returns the {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped\n   * extractor.\n   *\n   * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link\n   *     #init(BaseMediaChunkOutput)}.\n   * @return A {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped\n   *     extractor.\n   */\n  protected ChunkExtractorWrapper.TrackOutputProvider getTrackOutputProvider(\n      BaseMediaChunkOutput baseMediaChunkOutput) {\n    return baseMediaChunkOutput;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\n\n/**\n * A base class for {@link Chunk} implementations where the data should be loaded into a\n * {@code byte[]} before being consumed.\n */\npublic abstract class DataChunk extends Chunk {\n\n  private static final int READ_GRANULARITY = 16 * 1024;\n\n  private byte[] data;\n\n  private volatile boolean loadCanceled;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param type See {@link #type}.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param data An optional recycled array that can be used as a holder for the data.\n   */\n  public DataChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      int type,\n      Format trackFormat,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      byte[] data) {\n    super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData,\n        C.TIME_UNSET, C.TIME_UNSET);\n    this.data = data;\n  }\n\n  /**\n   * Returns the array in which the data is held.\n   * <p>\n   * This method should be used for recycling the holder only, and not for reading the data.\n   *\n   * @return The array in which the data is held.\n   */\n  public byte[] getDataHolder() {\n    return data;\n  }\n\n  // Loadable implementation\n\n  @Override\n  public final void cancelLoad() {\n    loadCanceled = true;\n  }\n\n  @Override\n  public final void load() throws IOException, InterruptedException {\n    try {\n      dataSource.open(dataSpec);\n      int limit = 0;\n      int bytesRead = 0;\n      while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) {\n        maybeExpandData(limit);\n        bytesRead = dataSource.read(data, limit, READ_GRANULARITY);\n        if (bytesRead != -1) {\n          limit += bytesRead;\n        }\n      }\n      if (!loadCanceled) {\n        consume(data, limit);\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n  }\n\n  /**\n   * Called by {@link #load()}. Implementations should override this method to consume the loaded\n   * data.\n   *\n   * @param data An array containing the data.\n   * @param limit The limit of the data.\n   * @throws IOException If an error occurs consuming the loaded data.\n   */\n  protected abstract void consume(byte[] data, int limit) throws IOException;\n\n  private void maybeExpandData(int limit) {\n    if (data == null) {\n      data = new byte[READ_GRANULARITY];\n    } else if (data.length < limit + READ_GRANULARITY) {\n      // The new length is calculated as (data.length + READ_GRANULARITY) rather than\n      // (limit + READ_GRANULARITY) in order to avoid small increments in the length.\n      data = Arrays.copyOf(data, data.length + READ_GRANULARITY);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorInput;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track.\n */\npublic final class InitializationChunk extends Chunk {\n\n  private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();\n\n  private final ChunkExtractorWrapper extractorWrapper;\n\n  private long nextLoadPosition;\n  private volatile boolean loadCanceled;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param extractorWrapper A wrapped extractor to use for parsing the initialization data.\n   */\n  public InitializationChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      Format trackFormat,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      ChunkExtractorWrapper extractorWrapper) {\n    super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason,\n        trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);\n    this.extractorWrapper = extractorWrapper;\n  }\n\n  // Loadable implementation.\n\n  @Override\n  public void cancelLoad() {\n    loadCanceled = true;\n  }\n\n  @SuppressWarnings(\"NonAtomicVolatileUpdate\")\n  @Override\n  public void load() throws IOException, InterruptedException {\n    DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);\n    try {\n      // Create and open the input.\n      ExtractorInput input = new DefaultExtractorInput(dataSource,\n          loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));\n      if (nextLoadPosition == 0) {\n        extractorWrapper.init(\n            /* trackOutputProvider= */ null,\n            /* startTimeUs= */ C.TIME_UNSET,\n            /* endTimeUs= */ C.TIME_UNSET);\n      }\n      // Load and decode the initialization data.\n      try {\n        Extractor extractor = extractorWrapper.extractor;\n        int result = Extractor.RESULT_CONTINUE;\n        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {\n          result = extractor.read(input, DUMMY_POSITION_HOLDER);\n        }\n        Assertions.checkState(result != Extractor.RESULT_SEEK);\n      } finally {\n        nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * An abstract base class for {@link Chunk}s that contain media samples.\n */\npublic abstract class MediaChunk extends Chunk {\n\n  /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */\n  public final long chunkIndex;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.\n   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.\n   * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.\n   */\n  public MediaChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      Format trackFormat,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs,\n      long chunkIndex) {\n    super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason,\n        trackSelectionData, startTimeUs, endTimeUs);\n    Assertions.checkNotNull(trackFormat);\n    this.chunkIndex = chunkIndex;\n  }\n\n  /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */\n  public long getNextChunkIndex() {\n    return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns whether the chunk has been fully loaded.\n   */\n  public abstract boolean isLoadCompleted();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.util.NoSuchElementException;\n\n/**\n * Iterator for media chunk sequences.\n *\n * <p>The iterator initially points in front of the first available element. The first call to\n * {@link #next()} moves the iterator to the first element. Check the return value of {@link\n * #next()} or {@link #isEnded()} to determine whether the iterator reached the end of the available\n * data.\n */\npublic interface MediaChunkIterator {\n\n  /** An empty media chunk iterator without available data. */\n  MediaChunkIterator EMPTY =\n      new MediaChunkIterator() {\n        @Override\n        public boolean isEnded() {\n          return true;\n        }\n\n        @Override\n        public boolean next() {\n          return false;\n        }\n\n        @Override\n        public DataSpec getDataSpec() {\n          throw new NoSuchElementException();\n        }\n\n        @Override\n        public long getChunkStartTimeUs() {\n          throw new NoSuchElementException();\n        }\n\n        @Override\n        public long getChunkEndTimeUs() {\n          throw new NoSuchElementException();\n        }\n\n        @Override\n        public void reset() {\n          // Do nothing.\n        }\n      };\n\n  /** Returns whether the iteration has reached the end of the available data. */\n  boolean isEnded();\n\n  /**\n   * Moves the iterator to the next media chunk.\n   *\n   * <p>Check the return value or {@link #isEnded()} to determine whether the iterator reached the\n   * end of the available data.\n   *\n   * @return Whether the iterator points to a media chunk with available data.\n   */\n  boolean next();\n\n  /**\n   * Returns the {@link DataSpec} used to load the media chunk.\n   *\n   * @throws NoSuchElementException If the method is called before the first call to\n   *     {@link #next()} or when {@link #isEnded()} is true.\n   */\n  DataSpec getDataSpec();\n\n  /**\n   * Returns the media start time of the chunk, in microseconds.\n   *\n   * @throws NoSuchElementException If the method is called before the first call to\n   *     {@link #next()} or when {@link #isEnded()} is true.\n   */\n  long getChunkStartTimeUs();\n\n  /**\n   * Returns the media end time of the chunk, in microseconds.\n   *\n   * @throws NoSuchElementException If the method is called before the first call to\n   *     {@link #next()} or when {@link #isEnded()} is true.\n   */\n  long getChunkEndTimeUs();\n\n  /** Resets the iterator to the initial position. */\n  void reset();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.util.List;\n\n/** A {@link MediaChunkIterator} which iterates over a {@link List} of {@link MediaChunk}s. */\npublic final class MediaChunkListIterator extends BaseMediaChunkIterator {\n\n  private final List<? extends MediaChunk> chunks;\n  private final boolean reverseOrder;\n\n  /**\n   * Creates iterator.\n   *\n   * @param chunks The list of chunks to iterate over.\n   * @param reverseOrder Whether to iterate in reverse order.\n   */\n  public MediaChunkListIterator(List<? extends MediaChunk> chunks, boolean reverseOrder) {\n    super(0, chunks.size() - 1);\n    this.chunks = chunks;\n    this.reverseOrder = reverseOrder;\n  }\n\n  @Override\n  public DataSpec getDataSpec() {\n    return getCurrentChunk().dataSpec;\n  }\n\n  @Override\n  public long getChunkStartTimeUs() {\n    return getCurrentChunk().startTimeUs;\n  }\n\n  @Override\n  public long getChunkEndTimeUs() {\n    return getCurrentChunk().endTimeUs;\n  }\n\n  private MediaChunk getCurrentChunk() {\n    int index = (int) super.getCurrentIndex();\n    if (reverseOrder) {\n      index = chunks.size() - 1 - index;\n    }\n    return chunks.get(index);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.chunk;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A {@link BaseMediaChunk} for chunks consisting of a single raw sample.\n */\npublic final class SingleSampleMediaChunk extends BaseMediaChunk {\n\n  private final int trackType;\n  private final Format sampleFormat;\n\n  private long nextLoadPosition;\n  private boolean loadCompleted;\n\n  /**\n   * @param dataSource The source from which the data should be loaded.\n   * @param dataSpec Defines the data to be loaded.\n   * @param trackFormat See {@link #trackFormat}.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.\n   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.\n   * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.\n   * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*}\n   *     constants.\n   * @param sampleFormat The {@link Format} of the sample in the chunk.\n   */\n  public SingleSampleMediaChunk(\n      DataSource dataSource,\n      DataSpec dataSpec,\n      Format trackFormat,\n      int trackSelectionReason,\n      Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs,\n      long chunkIndex,\n      int trackType,\n      Format sampleFormat) {\n    super(\n        dataSource,\n        dataSpec,\n        trackFormat,\n        trackSelectionReason,\n        trackSelectionData,\n        startTimeUs,\n        endTimeUs,\n        /* clippedStartTimeUs= */ C.TIME_UNSET,\n        /* clippedEndTimeUs= */ C.TIME_UNSET,\n        chunkIndex);\n    this.trackType = trackType;\n    this.sampleFormat = sampleFormat;\n  }\n\n\n  @Override\n  public boolean isLoadCompleted() {\n    return loadCompleted;\n  }\n\n  // Loadable implementation.\n\n  @Override\n  public void cancelLoad() {\n    // Do nothing.\n  }\n\n  @SuppressWarnings(\"NonAtomicVolatileUpdate\")\n  @Override\n  public void load() throws IOException, InterruptedException {\n    DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);\n    try {\n      // Create and open the input.\n      long length = dataSource.open(loadDataSpec);\n      if (length != C.LENGTH_UNSET) {\n        length += nextLoadPosition;\n      }\n      ExtractorInput extractorInput =\n          new DefaultExtractorInput(dataSource, nextLoadPosition, length);\n      BaseMediaChunkOutput output = getOutput();\n      output.setSampleOffsetUs(0);\n      TrackOutput trackOutput = output.track(0, trackType);\n      trackOutput.format(sampleFormat);\n      // Load the sample data.\n      int result = 0;\n      while (result != C.RESULT_END_OF_INPUT) {\n        nextLoadPosition += result;\n        result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);\n      }\n      int sampleSize = (int) nextLoadPosition;\n      trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n    loadCompleted = true;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.ChunkSource;\nimport com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.util.List;\n\n/**\n * An {@link ChunkSource} for DASH streams.\n */\npublic interface DashChunkSource extends ChunkSource {\n\n  /** Factory for {@link DashChunkSource}s. */\n  interface Factory {\n\n    /**\n     * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.\n     * @param manifest The initial manifest.\n     * @param periodIndex The index of the corresponding period in the manifest.\n     * @param adaptationSetIndices The indices of the corresponding adaptation sets in the period.\n     * @param trackSelection The track selection.\n     * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between\n     *     server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds,\n     *     specified as the server's unix time minus the local elapsed time. If unknown, set to 0.\n     * @param enableEventMessageTrack Whether to output an event message track.\n     * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output.\n     * @param transferListener The transfer listener which should be informed of any data transfers.\n     *     May be null if no listener is available.\n     * @return The created {@link DashChunkSource}.\n     */\n    DashChunkSource createDashChunkSource(\n        LoaderErrorThrower manifestLoaderErrorThrower,\n        DashManifest manifest,\n        int periodIndex,\n        int[] adaptationSetIndices,\n        TrackSelection trackSelection,\n        int type,\n        long elapsedRealtimeOffsetMs,\n        boolean enableEventMessageTrack,\n        List<Format> closedCaptionFormats,\n        @Nullable PlayerTrackEmsgHandler playerEmsgHandler,\n        @Nullable TransferListener transferListener);\n  }\n\n  /**\n   * Updates the manifest.\n   *\n   * @param newManifest The new manifest.\n   */\n  void updateManifest(DashManifest newManifest, int periodIndex);\n\n  /**\n   * Updates the track selection.\n   *\n   * @param trackSelection The new track selection instance. Must be equivalent to the previous one.\n   */\n  void updateTrackSelection(TrackSelection trackSelection);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestStaleException.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport java.io.IOException;\n\n/** Thrown when a live playback's manifest is stale and a new manifest could not be loaded. */\npublic final class DashManifestStaleException extends IOException {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport android.util.Pair;\nimport android.util.SparseIntArray;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.EmptySampleStream;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream;\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;\nimport com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;\nimport com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.source.dash.manifest.Descriptor;\nimport com.google.android.exoplayer2.source.dash.manifest.EventStream;\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.IdentityHashMap;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** A DASH {@link MediaPeriod}. */\n/* package */ final class DashMediaPeriod\n    implements MediaPeriod,\n        SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>,\n        ChunkSampleStream.ReleaseCallback<DashChunkSource> {\n\n  private static final Pattern CEA608_SERVICE_DESCRIPTOR_REGEX = Pattern.compile(\"CC([1-4])=(.+)\");\n\n  /* package */ final int id;\n  private final DashChunkSource.Factory chunkSourceFactory;\n  @Nullable private final TransferListener transferListener;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final long elapsedRealtimeOffsetMs;\n  private final LoaderErrorThrower manifestLoaderErrorThrower;\n  private final Allocator allocator;\n  private final TrackGroupArray trackGroups;\n  private final TrackGroupInfo[] trackGroupInfos;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final PlayerEmsgHandler playerEmsgHandler;\n  private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>\n      trackEmsgHandlerBySampleStream;\n  private final EventDispatcher eventDispatcher;\n\n  @Nullable private Callback callback;\n  private ChunkSampleStream<DashChunkSource>[] sampleStreams;\n  private EventSampleStream[] eventSampleStreams;\n  private SequenceableLoader compositeSequenceableLoader;\n  private DashManifest manifest;\n  private int periodIndex;\n  private List<EventStream> eventStreams;\n  private boolean notifiedReadingStarted;\n\n  public DashMediaPeriod(\n      int id,\n      DashManifest manifest,\n      int periodIndex,\n      DashChunkSource.Factory chunkSourceFactory,\n      @Nullable TransferListener transferListener,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      long elapsedRealtimeOffsetMs,\n      LoaderErrorThrower manifestLoaderErrorThrower,\n      Allocator allocator,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      PlayerEmsgCallback playerEmsgCallback) {\n    this.id = id;\n    this.manifest = manifest;\n    this.periodIndex = periodIndex;\n    this.chunkSourceFactory = chunkSourceFactory;\n    this.transferListener = transferListener;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;\n    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\n    this.allocator = allocator;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);\n    sampleStreams = newSampleStreamArray(0);\n    eventSampleStreams = new EventSampleStream[0];\n    trackEmsgHandlerBySampleStream = new IdentityHashMap<>();\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\n    Period period = manifest.getPeriod(periodIndex);\n    eventStreams = period.eventStreams;\n    Pair<TrackGroupArray, TrackGroupInfo[]> result =\n        buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams);\n    trackGroups = result.first;\n    trackGroupInfos = result.second;\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  /**\n   * Updates the {@link DashManifest} and the index of this period in the manifest.\n   *\n   * @param manifest The updated manifest.\n   * @param periodIndex the new index of this period in the updated manifest.\n   */\n  public void updateManifest(DashManifest manifest, int periodIndex) {\n    this.manifest = manifest;\n    this.periodIndex = periodIndex;\n    playerEmsgHandler.updateManifest(manifest);\n    if (sampleStreams != null) {\n      for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {\n        sampleStream.getChunkSource().updateManifest(manifest, periodIndex);\n      }\n      callback.onContinueLoadingRequested(this);\n    }\n    eventStreams = manifest.getPeriod(periodIndex).eventStreams;\n    for (EventSampleStream eventSampleStream : eventSampleStreams) {\n      for (EventStream eventStream : eventStreams) {\n        if (eventStream.id().equals(eventSampleStream.eventStreamId())) {\n          int lastPeriodIndex = manifest.getPeriodCount() - 1;\n          eventSampleStream.updateEventStream(\n              eventStream,\n              /* eventStreamAppendable= */ manifest.dynamic && periodIndex == lastPeriodIndex);\n          break;\n        }\n      }\n    }\n  }\n\n  public void release() {\n    playerEmsgHandler.release();\n    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {\n      sampleStream.release(this);\n    }\n    callback = null;\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  // ChunkSampleStream.ReleaseCallback implementation.\n\n  @Override\n  public synchronized void onSampleStreamReleased(ChunkSampleStream<DashChunkSource> stream) {\n    PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream);\n    if (trackEmsgHandler != null) {\n      trackEmsgHandler.release();\n    }\n  }\n\n  // MediaPeriod implementation.\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    callback.onPrepared(this);\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    manifestLoaderErrorThrower.maybeThrowError();\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return trackGroups;\n  }\n\n  @Override\n  public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {\n    List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;\n    List<StreamKey> streamKeys = new ArrayList<>();\n    for (TrackSelection trackSelection : trackSelections) {\n      int trackGroupIndex = trackGroups.indexOf(trackSelection.getTrackGroup());\n      TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];\n      if (trackGroupInfo.trackGroupCategory != TrackGroupInfo.CATEGORY_PRIMARY) {\n        // Ignore non-primary tracks.\n        continue;\n      }\n      int[] adaptationSetIndices = trackGroupInfo.adaptationSetIndices;\n      int[] trackIndices = new int[trackSelection.length()];\n      for (int i = 0; i < trackSelection.length(); i++) {\n        trackIndices[i] = trackSelection.getIndexInTrackGroup(i);\n      }\n      Arrays.sort(trackIndices);\n\n      int currentAdaptationSetIndex = 0;\n      int totalTracksInPreviousAdaptationSets = 0;\n      int tracksInCurrentAdaptationSet =\n          manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size();\n      for (int trackIndex : trackIndices) {\n        while (trackIndex >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) {\n          currentAdaptationSetIndex++;\n          totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet;\n          tracksInCurrentAdaptationSet =\n              manifestAdaptationSets\n                  .get(adaptationSetIndices[currentAdaptationSetIndex])\n                  .representations\n                  .size();\n        }\n        streamKeys.add(\n            new StreamKey(\n                periodIndex,\n                adaptationSetIndices[currentAdaptationSetIndex],\n                trackIndex - totalTracksInPreviousAdaptationSets));\n      }\n    }\n    return streamKeys;\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections);\n    releaseDisabledStreams(selections, mayRetainStreamFlags, streams);\n    releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex);\n    selectNewStreams(\n        selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex);\n\n    ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamList = new ArrayList<>();\n    ArrayList<EventSampleStream> eventSampleStreamList = new ArrayList<>();\n    for (SampleStream sampleStream : streams) {\n      if (sampleStream instanceof ChunkSampleStream) {\n        @SuppressWarnings(\"unchecked\")\n        ChunkSampleStream<DashChunkSource> stream =\n            (ChunkSampleStream<DashChunkSource>) sampleStream;\n        sampleStreamList.add(stream);\n      } else if (sampleStream instanceof EventSampleStream) {\n        eventSampleStreamList.add((EventSampleStream) sampleStream);\n      }\n    }\n    sampleStreams = newSampleStreamArray(sampleStreamList.size());\n    sampleStreamList.toArray(sampleStreams);\n    eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()];\n    eventSampleStreamList.toArray(eventSampleStreams);\n\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\n    return positionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {\n      sampleStream.discardBuffer(positionUs, toKeyframe);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    compositeSequenceableLoader.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    return compositeSequenceableLoader.continueLoading(positionUs);\n  }\n\n  @Override\n  public boolean isLoading() {\n    return compositeSequenceableLoader.isLoading();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return compositeSequenceableLoader.getNextLoadPositionUs();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    return C.TIME_UNSET;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return compositeSequenceableLoader.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {\n      sampleStream.seekToUs(positionUs);\n    }\n    for (EventSampleStream sampleStream : eventSampleStreams) {\n      sampleStream.seekToUs(positionUs);\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {\n      if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {\n        return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);\n      }\n    }\n    return positionUs;\n  }\n\n  // SequenceableLoader.Callback implementation.\n\n  @Override\n  public void onContinueLoadingRequested(ChunkSampleStream<DashChunkSource> sampleStream) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  // Internal methods.\n\n  private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) {\n    int[] streamIndexToTrackGroupIndex = new int[selections.length];\n    for (int i = 0; i < selections.length; i++) {\n      if (selections[i] != null) {\n        streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup());\n      } else {\n        streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET;\n      }\n    }\n    return streamIndexToTrackGroupIndex;\n  }\n\n  private void releaseDisabledStreams(\n      TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {\n    for (int i = 0; i < selections.length; i++) {\n      if (selections[i] == null || !mayRetainStreamFlags[i]) {\n        if (streams[i] instanceof ChunkSampleStream) {\n          @SuppressWarnings(\"unchecked\")\n          ChunkSampleStream<DashChunkSource> stream =\n              (ChunkSampleStream<DashChunkSource>) streams[i];\n          stream.release(this);\n        } else if (streams[i] instanceof EmbeddedSampleStream) {\n          ((EmbeddedSampleStream) streams[i]).release();\n        }\n        streams[i] = null;\n      }\n    }\n  }\n\n  private void releaseOrphanEmbeddedStreams(\n      TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) {\n        // We need to release an embedded stream if the corresponding primary stream is released.\n        int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);\n        boolean mayRetainStream;\n        if (primaryStreamIndex == C.INDEX_UNSET) {\n          // If the corresponding primary stream is not selected, we may retain an existing\n          // EmptySampleStream.\n          mayRetainStream = streams[i] instanceof EmptySampleStream;\n        } else {\n          // If the corresponding primary stream is selected, we may retain the embedded stream if\n          // the stream's parent still matches.\n          mayRetainStream =\n              (streams[i] instanceof EmbeddedSampleStream)\n                  && ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex];\n        }\n        if (!mayRetainStream) {\n          if (streams[i] instanceof EmbeddedSampleStream) {\n            ((EmbeddedSampleStream) streams[i]).release();\n          }\n          streams[i] = null;\n        }\n      }\n    }\n  }\n\n  private void selectNewStreams(\n      TrackSelection[] selections,\n      SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs,\n      int[] streamIndexToTrackGroupIndex) {\n    // Create newly selected primary and event streams.\n    for (int i = 0; i < selections.length; i++) {\n      TrackSelection selection = selections[i];\n      if (selection == null) {\n        continue;\n      }\n      if (streams[i] == null) {\n        // Create new stream for selection.\n        streamResetFlags[i] = true;\n        int trackGroupIndex = streamIndexToTrackGroupIndex[i];\n        TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];\n        if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {\n          streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs);\n        } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {\n          EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);\n          Format format = selection.getTrackGroup().getFormat(0);\n          streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic);\n        }\n      } else if (streams[i] instanceof ChunkSampleStream) {\n        // Update selection in existing stream.\n        @SuppressWarnings(\"unchecked\")\n        ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];\n        stream.getChunkSource().updateTrackSelection(selection);\n      }\n    }\n    // Create newly selected embedded streams from the corresponding primary stream. Note that this\n    // second pass is needed because the primary stream may not have been created yet in a first\n    // pass if the index of the primary stream is greater than the index of the embedded stream.\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] == null && selections[i] != null) {\n        int trackGroupIndex = streamIndexToTrackGroupIndex[i];\n        TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];\n        if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {\n          int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);\n          if (primaryStreamIndex == C.INDEX_UNSET) {\n            // If an embedded track is selected without the corresponding primary track, create an\n            // empty sample stream instead.\n            streams[i] = new EmptySampleStream();\n          } else {\n            streams[i] =\n                ((ChunkSampleStream) streams[primaryStreamIndex])\n                    .selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);\n          }\n        }\n      }\n    }\n  }\n\n  private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) {\n    int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex];\n    if (embeddedTrackGroupIndex == C.INDEX_UNSET) {\n      return C.INDEX_UNSET;\n    }\n    int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex;\n    for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) {\n      int trackGroupIndex = streamIndexToTrackGroupIndex[i];\n      if (trackGroupIndex == primaryTrackGroupIndex\n          && trackGroupInfos[trackGroupIndex].trackGroupCategory\n              == TrackGroupInfo.CATEGORY_PRIMARY) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(\n      DrmSessionManager<?> drmSessionManager,\n      List<AdaptationSet> adaptationSets,\n      List<EventStream> eventStreams) {\n    int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);\n\n    int primaryGroupCount = groupedAdaptationSetIndices.length;\n    boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];\n    Format[][] primaryGroupCea608TrackFormats = new Format[primaryGroupCount][];\n    int totalEmbeddedTrackGroupCount =\n        identifyEmbeddedTracks(\n            primaryGroupCount,\n            adaptationSets,\n            groupedAdaptationSetIndices,\n            primaryGroupHasEventMessageTrackFlags,\n            primaryGroupCea608TrackFormats);\n\n    int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size();\n    TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];\n    TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];\n\n    int trackGroupCount =\n        buildPrimaryAndEmbeddedTrackGroupInfos(\n            drmSessionManager,\n            adaptationSets,\n            groupedAdaptationSetIndices,\n            primaryGroupCount,\n            primaryGroupHasEventMessageTrackFlags,\n            primaryGroupCea608TrackFormats,\n            trackGroups,\n            trackGroupInfos);\n\n    buildManifestEventTrackGroupInfos(eventStreams, trackGroups, trackGroupInfos, trackGroupCount);\n\n    return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);\n  }\n\n  private static int[][] getGroupedAdaptationSetIndices(List<AdaptationSet> adaptationSets) {\n    int adaptationSetCount = adaptationSets.size();\n    SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);\n    for (int i = 0; i < adaptationSetCount; i++) {\n      idToIndexMap.put(adaptationSets.get(i).id, i);\n    }\n\n    int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][];\n    boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount];\n\n    int groupCount = 0;\n    for (int i = 0; i < adaptationSetCount; i++) {\n      if (adaptationSetUsedFlags[i]) {\n        // This adaptation set has already been included in a group.\n        continue;\n      }\n      adaptationSetUsedFlags[i] = true;\n      Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty(\n          adaptationSets.get(i).supplementalProperties);\n      if (adaptationSetSwitchingProperty == null) {\n        groupedAdaptationSetIndices[groupCount++] = new int[] {i};\n      } else {\n        String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, \",\");\n        int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];\n        adaptationSetIndices[0] = i;\n        int outputIndex = 1;\n        for (String adaptationSetId : extraAdaptationSetIds) {\n          int extraIndex =\n              idToIndexMap.get(Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1);\n          if (extraIndex != -1) {\n            adaptationSetUsedFlags[extraIndex] = true;\n            adaptationSetIndices[outputIndex] = extraIndex;\n            outputIndex++;\n          }\n        }\n        if (outputIndex < adaptationSetIndices.length) {\n          adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex);\n        }\n        groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;\n      }\n    }\n\n    return groupCount < adaptationSetCount\n        ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;\n  }\n\n  /**\n   * Iterates through list of primary track groups and identifies embedded tracks.\n   *\n   * @param primaryGroupCount The number of primary track groups.\n   * @param adaptationSets The list of {@link AdaptationSet} of the current DASH period.\n   * @param groupedAdaptationSetIndices The indices of {@link AdaptationSet} that belongs to the\n   *     same primary group, grouped in primary track groups order.\n   * @param primaryGroupHasEventMessageTrackFlags An output array to be filled with flags indicating\n   *     whether each of the primary track groups contains an embedded event message track.\n   * @param primaryGroupCea608TrackFormats An output array to be filled with track formats for\n   *     CEA-608 tracks embedded in each of the primary track groups.\n   * @return Total number of embedded track groups.\n   */\n  private static int identifyEmbeddedTracks(\n      int primaryGroupCount,\n      List<AdaptationSet> adaptationSets,\n      int[][] groupedAdaptationSetIndices,\n      boolean[] primaryGroupHasEventMessageTrackFlags,\n      Format[][] primaryGroupCea608TrackFormats) {\n    int numEmbeddedTrackGroups = 0;\n    for (int i = 0; i < primaryGroupCount; i++) {\n      if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {\n        primaryGroupHasEventMessageTrackFlags[i] = true;\n        numEmbeddedTrackGroups++;\n      }\n      primaryGroupCea608TrackFormats[i] =\n          getCea608TrackFormats(adaptationSets, groupedAdaptationSetIndices[i]);\n      if (primaryGroupCea608TrackFormats[i].length != 0) {\n        numEmbeddedTrackGroups++;\n      }\n    }\n    return numEmbeddedTrackGroups;\n  }\n\n  private static int buildPrimaryAndEmbeddedTrackGroupInfos(\n      DrmSessionManager<?> drmSessionManager,\n      List<AdaptationSet> adaptationSets,\n      int[][] groupedAdaptationSetIndices,\n      int primaryGroupCount,\n      boolean[] primaryGroupHasEventMessageTrackFlags,\n      Format[][] primaryGroupCea608TrackFormats,\n      TrackGroup[] trackGroups,\n      TrackGroupInfo[] trackGroupInfos) {\n    int trackGroupCount = 0;\n    for (int i = 0; i < primaryGroupCount; i++) {\n      int[] adaptationSetIndices = groupedAdaptationSetIndices[i];\n      List<Representation> representations = new ArrayList<>();\n      for (int adaptationSetIndex : adaptationSetIndices) {\n        representations.addAll(adaptationSets.get(adaptationSetIndex).representations);\n      }\n      Format[] formats = new Format[representations.size()];\n      for (int j = 0; j < formats.length; j++) {\n        Format format = representations.get(j).format;\n        DrmInitData drmInitData = format.drmInitData;\n        if (drmInitData != null) {\n          format =\n              format.copyWithExoMediaCryptoType(\n                  drmSessionManager.getExoMediaCryptoType(drmInitData));\n        }\n        formats[j] = format;\n      }\n\n      AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);\n      int primaryTrackGroupIndex = trackGroupCount++;\n      int eventMessageTrackGroupIndex =\n          primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET;\n      int cea608TrackGroupIndex =\n          primaryGroupCea608TrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;\n\n      trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats);\n      trackGroupInfos[primaryTrackGroupIndex] =\n          TrackGroupInfo.primaryTrack(\n              firstAdaptationSet.type,\n              adaptationSetIndices,\n              primaryTrackGroupIndex,\n              eventMessageTrackGroupIndex,\n              cea608TrackGroupIndex);\n      if (eventMessageTrackGroupIndex != C.INDEX_UNSET) {\n        Format format = Format.createSampleFormat(firstAdaptationSet.id + \":emsg\",\n            MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);\n        trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format);\n        trackGroupInfos[eventMessageTrackGroupIndex] =\n            TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex);\n      }\n      if (cea608TrackGroupIndex != C.INDEX_UNSET) {\n        trackGroups[cea608TrackGroupIndex] = new TrackGroup(primaryGroupCea608TrackFormats[i]);\n        trackGroupInfos[cea608TrackGroupIndex] =\n            TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex);\n      }\n    }\n    return trackGroupCount;\n  }\n\n  private static void buildManifestEventTrackGroupInfos(List<EventStream> eventStreams,\n      TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) {\n    for (int i = 0; i < eventStreams.size(); i++) {\n      EventStream eventStream = eventStreams.get(i);\n      Format format = Format.createSampleFormat(eventStream.id(), MimeTypes.APPLICATION_EMSG, null,\n          Format.NO_VALUE, null);\n      trackGroups[existingTrackGroupCount] = new TrackGroup(format);\n      trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i);\n    }\n  }\n\n  private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackGroupInfo trackGroupInfo,\n      TrackSelection selection, long positionUs) {\n    int embeddedTrackCount = 0;\n    boolean enableEventMessageTrack =\n        trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET;\n    TrackGroup embeddedEventMessageTrackGroup = null;\n    if (enableEventMessageTrack) {\n      embeddedEventMessageTrackGroup =\n          trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex);\n      embeddedTrackCount++;\n    }\n    boolean enableCea608Tracks = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET;\n    TrackGroup embeddedCea608TrackGroup = null;\n    if (enableCea608Tracks) {\n      embeddedCea608TrackGroup = trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex);\n      embeddedTrackCount += embeddedCea608TrackGroup.length;\n    }\n\n    Format[] embeddedTrackFormats = new Format[embeddedTrackCount];\n    int[] embeddedTrackTypes = new int[embeddedTrackCount];\n    embeddedTrackCount = 0;\n    if (enableEventMessageTrack) {\n      embeddedTrackFormats[embeddedTrackCount] = embeddedEventMessageTrackGroup.getFormat(0);\n      embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA;\n      embeddedTrackCount++;\n    }\n    List<Format> embeddedCea608TrackFormats = new ArrayList<>();\n    if (enableCea608Tracks) {\n      for (int i = 0; i < embeddedCea608TrackGroup.length; i++) {\n        embeddedTrackFormats[embeddedTrackCount] = embeddedCea608TrackGroup.getFormat(i);\n        embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;\n        embeddedCea608TrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);\n        embeddedTrackCount++;\n      }\n    }\n\n    PlayerTrackEmsgHandler trackPlayerEmsgHandler =\n        manifest.dynamic && enableEventMessageTrack\n            ? playerEmsgHandler.newPlayerTrackEmsgHandler()\n            : null;\n    DashChunkSource chunkSource =\n        chunkSourceFactory.createDashChunkSource(\n            manifestLoaderErrorThrower,\n            manifest,\n            periodIndex,\n            trackGroupInfo.adaptationSetIndices,\n            selection,\n            trackGroupInfo.trackType,\n            elapsedRealtimeOffsetMs,\n            enableEventMessageTrack,\n            embeddedCea608TrackFormats,\n            trackPlayerEmsgHandler,\n            transferListener);\n    ChunkSampleStream<DashChunkSource> stream =\n        new ChunkSampleStream<>(\n            trackGroupInfo.trackType,\n            embeddedTrackTypes,\n            embeddedTrackFormats,\n            chunkSource,\n            this,\n            allocator,\n            positionUs,\n            drmSessionManager,\n            loadErrorHandlingPolicy,\n            eventDispatcher);\n    synchronized (this) {\n      // The map is also accessed on the loading thread so synchronize access.\n      trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);\n    }\n    return stream;\n  }\n\n  private static Descriptor findAdaptationSetSwitchingProperty(List<Descriptor> descriptors) {\n    for (int i = 0; i < descriptors.size(); i++) {\n      Descriptor descriptor = descriptors.get(i);\n      if (\"urn:mpeg:dash:adaptation-set-switching:2016\".equals(descriptor.schemeIdUri)) {\n        return descriptor;\n      }\n    }\n    return null;\n  }\n\n  private static boolean hasEventMessageTrack(List<AdaptationSet> adaptationSets,\n      int[] adaptationSetIndices) {\n    for (int i : adaptationSetIndices) {\n      List<Representation> representations = adaptationSets.get(i).representations;\n      for (int j = 0; j < representations.size(); j++) {\n        Representation representation = representations.get(j);\n        if (!representation.inbandEventStreams.isEmpty()) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  private static Format[] getCea608TrackFormats(\n      List<AdaptationSet> adaptationSets, int[] adaptationSetIndices) {\n    for (int i : adaptationSetIndices) {\n      AdaptationSet adaptationSet = adaptationSets.get(i);\n      List<Descriptor> descriptors = adaptationSets.get(i).accessibilityDescriptors;\n      for (int j = 0; j < descriptors.size(); j++) {\n        Descriptor descriptor = descriptors.get(j);\n        if (\"urn:scte:dash:cc:cea-608:2015\".equals(descriptor.schemeIdUri)) {\n          String value = descriptor.value;\n          if (value == null) {\n            // There are embedded CEA-608 tracks, but service information is not declared.\n            return new Format[] {buildCea608TrackFormat(adaptationSet.id)};\n          }\n          String[] services = Util.split(value, \";\");\n          Format[] formats = new Format[services.length];\n          for (int k = 0; k < services.length; k++) {\n            Matcher matcher = CEA608_SERVICE_DESCRIPTOR_REGEX.matcher(services[k]);\n            if (!matcher.matches()) {\n              // If we can't parse service information for all services, assume a single track.\n              return new Format[] {buildCea608TrackFormat(adaptationSet.id)};\n            }\n            formats[k] =\n                buildCea608TrackFormat(\n                    adaptationSet.id,\n                    /* language= */ matcher.group(2),\n                    /* accessibilityChannel= */ Integer.parseInt(matcher.group(1)));\n          }\n          return formats;\n        }\n      }\n    }\n    return new Format[0];\n  }\n\n  private static Format buildCea608TrackFormat(int adaptationSetId) {\n    return buildCea608TrackFormat(\n        adaptationSetId, /* language= */ null, /* accessibilityChannel= */ Format.NO_VALUE);\n  }\n\n  private static Format buildCea608TrackFormat(\n      int adaptationSetId, String language, int accessibilityChannel) {\n    return Format.createTextSampleFormat(\n        adaptationSetId\n            + \":cea608\"\n            + (accessibilityChannel != Format.NO_VALUE ? \":\" + accessibilityChannel : \"\"),\n        MimeTypes.APPLICATION_CEA608,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* selectionFlags= */ 0,\n        language,\n        accessibilityChannel,\n        /* drmInitData= */ null,\n        Format.OFFSET_SAMPLE_RELATIVE,\n        /* initializationData= */ null);\n  }\n\n  // We won't assign the array to a variable that erases the generic type, and then write into it.\n  @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n  private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) {\n    return new ChunkSampleStream[length];\n  }\n\n  private static final class TrackGroupInfo {\n\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({CATEGORY_PRIMARY, CATEGORY_EMBEDDED, CATEGORY_MANIFEST_EVENTS})\n    public @interface TrackGroupCategory {}\n\n    /**\n     * A normal track group that has its samples drawn from the stream.\n     * For example: a video Track Group or an audio Track Group.\n     */\n    private static final int CATEGORY_PRIMARY = 0;\n\n    /**\n     * A track group whose samples are embedded within one of the primary streams. For example: an\n     * EMSG track has its sample embedded in emsg atoms in one of the primary streams.\n     */\n    private static final int CATEGORY_EMBEDDED = 1;\n\n    /**\n     * A track group that has its samples listed explicitly in the DASH manifest file.\n     * For example: an EventStream track has its sample (Events) included directly in the DASH\n     * manifest file.\n     */\n    private static final int CATEGORY_MANIFEST_EVENTS = 2;\n\n    public final int[] adaptationSetIndices;\n    public final int trackType;\n    @TrackGroupCategory public final int trackGroupCategory;\n\n    public final int eventStreamGroupIndex;\n    public final int primaryTrackGroupIndex;\n    public final int embeddedEventMessageTrackGroupIndex;\n    public final int embeddedCea608TrackGroupIndex;\n\n    public static TrackGroupInfo primaryTrack(\n        int trackType,\n        int[] adaptationSetIndices,\n        int primaryTrackGroupIndex,\n        int embeddedEventMessageTrackGroupIndex,\n        int embeddedCea608TrackGroupIndex) {\n      return new TrackGroupInfo(\n          trackType,\n          CATEGORY_PRIMARY,\n          adaptationSetIndices,\n          primaryTrackGroupIndex,\n          embeddedEventMessageTrackGroupIndex,\n          embeddedCea608TrackGroupIndex,\n          /* eventStreamGroupIndex= */ -1);\n    }\n\n    public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices,\n        int primaryTrackGroupIndex) {\n      return new TrackGroupInfo(\n          C.TRACK_TYPE_METADATA,\n          CATEGORY_EMBEDDED,\n          adaptationSetIndices,\n          primaryTrackGroupIndex,\n          C.INDEX_UNSET,\n          C.INDEX_UNSET,\n          /* eventStreamGroupIndex= */ -1);\n    }\n\n    public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices,\n        int primaryTrackGroupIndex) {\n      return new TrackGroupInfo(\n          C.TRACK_TYPE_TEXT,\n          CATEGORY_EMBEDDED,\n          adaptationSetIndices,\n          primaryTrackGroupIndex,\n          C.INDEX_UNSET,\n          C.INDEX_UNSET,\n          /* eventStreamGroupIndex= */ -1);\n    }\n\n    public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) {\n      return new TrackGroupInfo(\n          C.TRACK_TYPE_METADATA,\n          CATEGORY_MANIFEST_EVENTS,\n          new int[0],\n          /* primaryTrackGroupIndex= */ -1,\n          C.INDEX_UNSET,\n          C.INDEX_UNSET,\n          eventStreamIndex);\n    }\n\n    private TrackGroupInfo(\n        int trackType,\n        @TrackGroupCategory int trackGroupCategory,\n        int[] adaptationSetIndices,\n        int primaryTrackGroupIndex,\n        int embeddedEventMessageTrackGroupIndex,\n        int embeddedCea608TrackGroupIndex,\n        int eventStreamGroupIndex) {\n      this.trackType = trackType;\n      this.adaptationSetIndices = adaptationSetIndices;\n      this.trackGroupCategory = trackGroupCategory;\n      this.primaryTrackGroupIndex = primaryTrackGroupIndex;\n      this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex;\n      this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex;\n      this.eventStreamGroupIndex = eventStreamGroupIndex;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.text.TextUtils;\nimport android.util.SparseArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.offline.FilteringManifestParser;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.BaseMediaSource;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.MediaSourceFactory;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;\nimport com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.charset.Charset;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/** A DASH {@link MediaSource}. */\npublic final class DashMediaSource extends BaseMediaSource {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.dash\");\n  }\n\n  /** Factory for {@link DashMediaSource}s. */\n  public static final class Factory implements MediaSourceFactory {\n\n    private final DashChunkSource.Factory chunkSourceFactory;\n    @Nullable private final DataSource.Factory manifestDataSourceFactory;\n\n    private DrmSessionManager<?> drmSessionManager;\n    @Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;\n    @Nullable private List<StreamKey> streamKeys;\n    private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private long livePresentationDelayMs;\n    private boolean livePresentationDelayOverridesManifest;\n    private boolean isCreateCalled;\n    @Nullable private Object tag;\n\n    /**\n     * Creates a new factory for {@link DashMediaSource}s.\n     *\n     * @param dataSourceFactory A factory for {@link DataSource} instances that will be used to load\n     *     manifest and media data.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this(new DefaultDashChunkSource.Factory(dataSourceFactory), dataSourceFactory);\n    }\n\n    /**\n     * Creates a new factory for {@link DashMediaSource}s.\n     *\n     * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n     * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n     *     to load (and refresh) the manifest. May be {@code null} if the factory will only ever be\n     *     used to create create media sources with sideloaded manifests via {@link\n     *     #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}.\n     */\n    public Factory(\n        DashChunkSource.Factory chunkSourceFactory,\n        @Nullable DataSource.Factory manifestDataSourceFactory) {\n      this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory);\n      this.manifestDataSourceFactory = manifestDataSourceFactory;\n      drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;\n      compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link\n     * com.google.android.exoplayer2.Timeline} of the source as {@link\n     * com.google.android.exoplayer2.Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTag(@Nullable Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The\n     * default value is {@link DrmSessionManager#DUMMY}.\n     *\n     * @param drmSessionManager The {@link DrmSessionManager}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {\n      Assertions.checkState(!isCreateCalled);\n      this.drmSessionManager = drmSessionManager;\n      return this;\n    }\n\n    /**\n     * Sets the minimum number of times to retry if a loading error occurs. See {@link\n     * #setLoadErrorHandlingPolicy} for the default value.\n     *\n     * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\n     * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\n     * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\n     *\n     * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\n     */\n    @Deprecated\n    public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\n      return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)}. */\n    @Deprecated\n    @SuppressWarnings(\"deprecation\")\n    public Factory setLivePresentationDelayMs(long livePresentationDelayMs) {\n      if (livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) {\n        return setLivePresentationDelayMs(DEFAULT_LIVE_PRESENTATION_DELAY_MS, false);\n      } else {\n        return setLivePresentationDelayMs(livePresentationDelayMs, true);\n      }\n    }\n\n    /**\n     * Sets the duration in milliseconds by which the default start position should precede the end\n     * of the live window for live playbacks. The {@code overridesManifest} parameter specifies\n     * whether the value is used in preference to one in the manifest, if present. The default value\n     * is {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}, and by default {@code overridesManifest} is\n     * false.\n     *\n     * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n     *     default start position should precede the end of the live window.\n     * @param overridesManifest Whether the value is used in preference to one in the manifest, if\n     *     present.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLivePresentationDelayMs(\n        long livePresentationDelayMs, boolean overridesManifest) {\n      Assertions.checkState(!isCreateCalled);\n      this.livePresentationDelayMs = livePresentationDelayMs;\n      this.livePresentationDelayOverridesManifest = overridesManifest;\n      return this;\n    }\n\n    /**\n     * Sets the manifest parser to parse loaded manifest data when loading a manifest URI.\n     *\n     * @param manifestParser A parser for loaded manifest data.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setManifestParser(\n        ParsingLoadable.Parser<? extends DashManifest> manifestParser) {\n      Assertions.checkState(!isCreateCalled);\n      this.manifestParser = Assertions.checkNotNull(manifestParser);\n      return this;\n    }\n\n    /**\n     * Sets the factory to create composite {@link SequenceableLoader}s for when this media source\n     * loads data from multiple streams (video, audio etc...). The default is an instance of {@link\n     * DefaultCompositeSequenceableLoaderFactory}.\n     *\n     * @param compositeSequenceableLoaderFactory A factory to create composite {@link\n     *     SequenceableLoader}s for when this media source loads data from multiple streams (video,\n     *     audio etc...).\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setCompositeSequenceableLoaderFactory(\n        CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.compositeSequenceableLoaderFactory =\n          Assertions.checkNotNull(compositeSequenceableLoaderFactory);\n      return this;\n    }\n\n    /**\n     * Returns a new {@link DashMediaSource} using the current parameters and the specified\n     * sideloaded manifest.\n     *\n     * @param manifest The manifest. {@link DashManifest#dynamic} must be false.\n     * @return The new {@link DashMediaSource}.\n     * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true.\n     */\n    public DashMediaSource createMediaSource(DashManifest manifest) {\n      Assertions.checkArgument(!manifest.dynamic);\n      isCreateCalled = true;\n      if (streamKeys != null && !streamKeys.isEmpty()) {\n        manifest = manifest.copy(streamKeys);\n      }\n      return new DashMediaSource(\n          manifest,\n          /* manifestUri= */ null,\n          /* manifestDataSourceFactory= */ null,\n          /* manifestParser= */ null,\n          chunkSourceFactory,\n          compositeSequenceableLoaderFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          livePresentationDelayOverridesManifest,\n          tag);\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(DashManifest)} and {@link\n     *     #addEventListener(Handler, MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public DashMediaSource createMediaSource(\n        DashManifest manifest,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      DashMediaSource mediaSource = createMediaSource(manifest);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,\n     *     MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public DashMediaSource createMediaSource(\n        Uri manifestUri,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      DashMediaSource mediaSource = createMediaSource(manifestUri);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    /**\n     * Returns a new {@link DashMediaSource} using the current parameters.\n     *\n     * @param manifestUri The manifest {@link Uri}.\n     * @return The new {@link DashMediaSource}.\n     */\n    @Override\n    public DashMediaSource createMediaSource(Uri manifestUri) {\n      isCreateCalled = true;\n      if (manifestParser == null) {\n        manifestParser = new DashManifestParser();\n      }\n      if (streamKeys != null) {\n        manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);\n      }\n      return new DashMediaSource(\n          /* manifest= */ null,\n          Assertions.checkNotNull(manifestUri),\n          manifestDataSourceFactory,\n          manifestParser,\n          chunkSourceFactory,\n          compositeSequenceableLoaderFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          livePresentationDelayOverridesManifest,\n          tag);\n    }\n\n    @Override\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\n      return this;\n    }\n\n    @Override\n    public int[] getSupportedTypes() {\n      return new int[] {C.TYPE_DASH};\n    }\n  }\n\n  /**\n   * The default presentation delay for live streams. The presentation delay is the duration by\n   * which the default start position precedes the end of the live window.\n   */\n  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000;\n  /** @deprecated Use {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. */\n  @Deprecated\n  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS =\n      DEFAULT_LIVE_PRESENTATION_DELAY_MS;\n  /** @deprecated Use of this parameter is no longer necessary. */\n  @Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1;\n\n  /**\n   * The interval in milliseconds between invocations of {@link\n   * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link\n   * Timeline} is changing dynamically (for example, for incomplete live streams).\n   */\n  private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000;\n  /**\n   * The minimum default start position for live streams, relative to the start of the live window.\n   */\n  private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000;\n\n  private static final String TAG = \"DashMediaSource\";\n\n  private final boolean sideloadedManifest;\n  private final DataSource.Factory manifestDataSourceFactory;\n  private final DashChunkSource.Factory chunkSourceFactory;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final long livePresentationDelayMs;\n  private final boolean livePresentationDelayOverridesManifest;\n  private final EventDispatcher manifestEventDispatcher;\n  private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;\n  private final ManifestCallback manifestCallback;\n  private final Object manifestUriLock;\n  private final SparseArray<DashMediaPeriod> periodsById;\n  private final Runnable refreshManifestRunnable;\n  private final Runnable simulateManifestRefreshRunnable;\n  private final PlayerEmsgCallback playerEmsgCallback;\n  private final LoaderErrorThrower manifestLoadErrorThrower;\n  @Nullable private final Object tag;\n\n  private DataSource dataSource;\n  private Loader loader;\n  @Nullable private TransferListener mediaTransferListener;\n\n  private IOException manifestFatalError;\n  private Handler handler;\n\n  private Uri initialManifestUri;\n  private Uri manifestUri;\n  private DashManifest manifest;\n  private boolean manifestLoadPending;\n  private long manifestLoadStartTimestampMs;\n  private long manifestLoadEndTimestampMs;\n  private long elapsedRealtimeOffsetMs;\n\n  private int staleManifestReloadAttempt;\n  private long expiredManifestPublishTimeUs;\n\n  private int firstPeriodId;\n\n  /**\n   * Constructs an instance to play a given {@link DashManifest}, which must be static.\n   *\n   * @param manifest The manifest. {@link DashManifest#dynamic} must be false.\n   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DashMediaSource(\n      DashManifest manifest,\n      DashChunkSource.Factory chunkSourceFactory,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        chunkSourceFactory,\n        DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * Constructs an instance to play a given {@link DashManifest}, which must be static.\n   *\n   * @param manifest The manifest. {@link DashManifest#dynamic} must be false.\n   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public DashMediaSource(\n      DashManifest manifest,\n      DashChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        /* manifestUri= */ null,\n        /* manifestDataSourceFactory= */ null,\n        /* manifestParser= */ null,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\n        DrmSessionManager.getDummyDrmSessionManager(),\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        DEFAULT_LIVE_PRESENTATION_DELAY_MS,\n        /* livePresentationDelayOverridesManifest= */ false,\n        /* tag= */ null);\n    if (eventHandler != null && eventListener != null) {\n      addEventListener(eventHandler, eventListener);\n    }\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or\n   * static.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DashMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      DashChunkSource.Factory chunkSourceFactory,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifestUri,\n        manifestDataSourceFactory,\n        chunkSourceFactory,\n        DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT,\n        DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or\n   * static.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n   *     default start position should precede the end of the live window. Use {@link\n   *     #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the\n   *     manifest, if present.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DashMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      DashChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      long livePresentationDelayMs,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifestUri,\n        manifestDataSourceFactory,\n        new DashManifestParser(),\n        chunkSourceFactory,\n        minLoadableRetryCount,\n        livePresentationDelayMs,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or\n   * static.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param manifestParser A parser for loaded manifest data.\n   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n   *     default start position should precede the end of the live window. Use {@link\n   *     #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the\n   *     manifest, if present.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DashMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      ParsingLoadable.Parser<? extends DashManifest> manifestParser,\n      DashChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      long livePresentationDelayMs,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        /* manifest= */ null,\n        manifestUri,\n        manifestDataSourceFactory,\n        manifestParser,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\n        DrmSessionManager.getDummyDrmSessionManager(),\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS\n            ? DEFAULT_LIVE_PRESENTATION_DELAY_MS\n            : livePresentationDelayMs,\n        livePresentationDelayMs != DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS,\n        /* tag= */ null);\n    if (eventHandler != null && eventListener != null) {\n      addEventListener(eventHandler, eventListener);\n    }\n  }\n\n  private DashMediaSource(\n      @Nullable DashManifest manifest,\n      @Nullable Uri manifestUri,\n      @Nullable DataSource.Factory manifestDataSourceFactory,\n      @Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser,\n      DashChunkSource.Factory chunkSourceFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      long livePresentationDelayMs,\n      boolean livePresentationDelayOverridesManifest,\n      @Nullable Object tag) {\n    this.initialManifestUri = manifestUri;\n    this.manifest = manifest;\n    this.manifestUri = manifestUri;\n    this.manifestDataSourceFactory = manifestDataSourceFactory;\n    this.manifestParser = manifestParser;\n    this.chunkSourceFactory = chunkSourceFactory;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.livePresentationDelayMs = livePresentationDelayMs;\n    this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    this.tag = tag;\n    sideloadedManifest = manifest != null;\n    manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);\n    manifestUriLock = new Object();\n    periodsById = new SparseArray<>();\n    playerEmsgCallback = new DefaultPlayerEmsgCallback();\n    expiredManifestPublishTimeUs = C.TIME_UNSET;\n    if (sideloadedManifest) {\n      Assertions.checkState(!manifest.dynamic);\n      manifestCallback = null;\n      refreshManifestRunnable = null;\n      simulateManifestRefreshRunnable = null;\n      manifestLoadErrorThrower = new LoaderErrorThrower.Dummy();\n    } else {\n      manifestCallback = new ManifestCallback();\n      manifestLoadErrorThrower = new ManifestLoadErrorThrower();\n      refreshManifestRunnable = this::startLoadingManifest;\n      simulateManifestRefreshRunnable = () -> processManifest(false);\n    }\n  }\n\n  /**\n   * Manually replaces the manifest {@link Uri}.\n   *\n   * @param manifestUri The replacement manifest {@link Uri}.\n   */\n  public void replaceManifestUri(Uri manifestUri) {\n    synchronized (manifestUriLock) {\n      this.manifestUri = manifestUri;\n      this.initialManifestUri = manifestUri;\n    }\n  }\n\n  // MediaSource implementation.\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\n    drmSessionManager.prepare();\n    if (sideloadedManifest) {\n      processManifest(false);\n    } else {\n      dataSource = manifestDataSourceFactory.createDataSource();\n      loader = new Loader(\"Loader:DashMediaSource\");\n      handler = new Handler();\n      startLoadingManifest();\n    }\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    manifestLoadErrorThrower.maybeThrowError();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(\n      MediaPeriodId periodId, Allocator allocator, long startPositionUs) {\n    int periodIndex = (Integer) periodId.periodUid - firstPeriodId;\n    EventDispatcher periodEventDispatcher =\n        createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs);\n    DashMediaPeriod mediaPeriod =\n        new DashMediaPeriod(\n            firstPeriodId + periodIndex,\n            manifest,\n            periodIndex,\n            chunkSourceFactory,\n            mediaTransferListener,\n            drmSessionManager,\n            loadErrorHandlingPolicy,\n            periodEventDispatcher,\n            elapsedRealtimeOffsetMs,\n            manifestLoadErrorThrower,\n            allocator,\n            compositeSequenceableLoaderFactory,\n            playerEmsgCallback);\n    periodsById.put(mediaPeriod.id, mediaPeriod);\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    DashMediaPeriod dashMediaPeriod = (DashMediaPeriod) mediaPeriod;\n    dashMediaPeriod.release();\n    periodsById.remove(dashMediaPeriod.id);\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    manifestLoadPending = false;\n    dataSource = null;\n    if (loader != null) {\n      loader.release();\n      loader = null;\n    }\n    manifestLoadStartTimestampMs = 0;\n    manifestLoadEndTimestampMs = 0;\n    manifest = sideloadedManifest ? manifest : null;\n    manifestUri = initialManifestUri;\n    manifestFatalError = null;\n    if (handler != null) {\n      handler.removeCallbacksAndMessages(null);\n      handler = null;\n    }\n    elapsedRealtimeOffsetMs = 0;\n    staleManifestReloadAttempt = 0;\n    expiredManifestPublishTimeUs = C.TIME_UNSET;\n    firstPeriodId = 0;\n    periodsById.clear();\n    drmSessionManager.release();\n  }\n\n  // PlayerEmsgCallback callbacks.\n\n  /* package */ void onDashManifestRefreshRequested() {\n    handler.removeCallbacks(simulateManifestRefreshRunnable);\n    startLoadingManifest();\n  }\n\n  /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {\n    if (this.expiredManifestPublishTimeUs == C.TIME_UNSET\n        || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) {\n      this.expiredManifestPublishTimeUs = expiredManifestPublishTimeUs;\n    }\n  }\n\n  // Loadable callbacks.\n\n  /* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,\n      long elapsedRealtimeMs, long loadDurationMs) {\n    manifestEventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    DashManifest newManifest = loadable.getResult();\n\n    int oldPeriodCount = manifest == null ? 0 : manifest.getPeriodCount();\n    int removedPeriodCount = 0;\n    long newFirstPeriodStartTimeMs = newManifest.getPeriod(0).startMs;\n    while (removedPeriodCount < oldPeriodCount\n        && manifest.getPeriod(removedPeriodCount).startMs < newFirstPeriodStartTimeMs) {\n      removedPeriodCount++;\n    }\n\n    if (newManifest.dynamic) {\n      boolean isManifestStale = false;\n      if (oldPeriodCount - removedPeriodCount > newManifest.getPeriodCount()) {\n        // After discarding old periods, we should never have more periods than listed in the new\n        // manifest. That would mean that a previously announced period is no longer advertised. If\n        // this condition occurs, assume that we are hitting a manifest server that is out of sync\n        // and\n        // behind.\n        Log.w(TAG, \"Loaded out of sync manifest\");\n        isManifestStale = true;\n      } else if (expiredManifestPublishTimeUs != C.TIME_UNSET\n          && newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs) {\n        // If we receive a dynamic manifest that's older than expected (i.e. its publish time has\n        // expired, or it's dynamic and we know the presentation has ended), then this manifest is\n        // stale.\n        Log.w(\n            TAG,\n            \"Loaded stale dynamic manifest: \"\n                + newManifest.publishTimeMs\n                + \", \"\n                + expiredManifestPublishTimeUs);\n        isManifestStale = true;\n      }\n\n      if (isManifestStale) {\n        if (staleManifestReloadAttempt++\n            < loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)) {\n          scheduleManifestRefresh(getManifestLoadRetryDelayMillis());\n        } else {\n          manifestFatalError = new DashManifestStaleException();\n        }\n        return;\n      }\n      staleManifestReloadAttempt = 0;\n    }\n\n    manifest = newManifest;\n    manifestLoadPending &= manifest.dynamic;\n    manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;\n    manifestLoadEndTimestampMs = elapsedRealtimeMs;\n    if (manifest.location != null) {\n      synchronized (manifestUriLock) {\n        // This condition checks that replaceManifestUri wasn't called between the start and end of\n        // this load. If it was, we ignore the manifest location and prefer the manual replacement.\n        @SuppressWarnings(\"ReferenceEquality\")\n        boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri;\n        if (isSameUriInstance) {\n          manifestUri = manifest.location;\n        }\n      }\n    }\n\n    if (oldPeriodCount == 0) {\n      if (manifest.dynamic && manifest.utcTiming != null) {\n        resolveUtcTimingElement(manifest.utcTiming);\n      } else {\n        processManifest(true);\n      }\n    } else {\n      firstPeriodId += removedPeriodCount;\n      processManifest(true);\n    }\n  }\n\n  /* package */ LoadErrorAction onManifestLoadError(\n      ParsingLoadable<DashManifest> loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long retryDelayMs =\n        loadErrorHandlingPolicy.getRetryDelayMsFor(\n            C.DATA_TYPE_MANIFEST, loadDurationMs, error, errorCount);\n    LoadErrorAction loadErrorAction =\n        retryDelayMs == C.TIME_UNSET\n            ? Loader.DONT_RETRY_FATAL\n            : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);\n    manifestEventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded(),\n        error,\n        !loadErrorAction.isRetry());\n    return loadErrorAction;\n  }\n\n  /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable<Long> loadable,\n      long elapsedRealtimeMs, long loadDurationMs) {\n    manifestEventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs);\n  }\n\n  /* package */ LoadErrorAction onUtcTimestampLoadError(\n      ParsingLoadable<Long> loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error) {\n    manifestEventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded(),\n        error,\n        true);\n    onUtcTimestampResolutionError(error);\n    return Loader.DONT_RETRY;\n  }\n\n  /* package */ void onLoadCanceled(ParsingLoadable<?> loadable, long elapsedRealtimeMs,\n      long loadDurationMs) {\n    manifestEventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n  }\n\n  // Internal methods.\n\n  private void resolveUtcTimingElement(UtcTimingElement timingElement) {\n    String scheme = timingElement.schemeIdUri;\n    if (Util.areEqual(scheme, \"urn:mpeg:dash:utc:direct:2014\")\n        || Util.areEqual(scheme, \"urn:mpeg:dash:utc:direct:2012\")) {\n      resolveUtcTimingElementDirect(timingElement);\n    } else if (Util.areEqual(scheme, \"urn:mpeg:dash:utc:http-iso:2014\")\n        || Util.areEqual(scheme, \"urn:mpeg:dash:utc:http-iso:2012\")) {\n      resolveUtcTimingElementHttp(timingElement, new Iso8601Parser());\n    } else if (Util.areEqual(scheme, \"urn:mpeg:dash:utc:http-xsdate:2014\")\n        || Util.areEqual(scheme, \"urn:mpeg:dash:utc:http-xsdate:2012\")) {\n      resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());\n    } else {\n      // Unsupported scheme.\n      onUtcTimestampResolutionError(new IOException(\"Unsupported UTC timing scheme\"));\n    }\n  }\n\n  private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) {\n    try {\n      long utcTimestampMs = Util.parseXsDateTime(timingElement.value);\n      onUtcTimestampResolved(utcTimestampMs - manifestLoadEndTimestampMs);\n    } catch (ParserException e) {\n      onUtcTimestampResolutionError(e);\n    }\n  }\n\n  private void resolveUtcTimingElementHttp(UtcTimingElement timingElement,\n      ParsingLoadable.Parser<Long> parser) {\n    startLoading(new ParsingLoadable<>(dataSource, Uri.parse(timingElement.value),\n        C.DATA_TYPE_TIME_SYNCHRONIZATION, parser), new UtcTimestampCallback(), 1);\n  }\n\n  private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {\n    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;\n    processManifest(true);\n  }\n\n  private void onUtcTimestampResolutionError(IOException error) {\n    Log.e(TAG, \"Failed to resolve UtcTiming element.\", error);\n    // Be optimistic and continue in the hope that the device clock is correct.\n    processManifest(true);\n  }\n\n  private void processManifest(boolean scheduleRefresh) {\n    // Update any periods.\n    for (int i = 0; i < periodsById.size(); i++) {\n      int id = periodsById.keyAt(i);\n      if (id >= firstPeriodId) {\n        periodsById.valueAt(i).updateManifest(manifest, id - firstPeriodId);\n      } else {\n        // This period has been removed from the manifest so it doesn't need to be updated.\n      }\n    }\n    // Update the window.\n    boolean windowChangingImplicitly = false;\n    int lastPeriodIndex = manifest.getPeriodCount() - 1;\n    PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0),\n        manifest.getPeriodDurationUs(0));\n    PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(\n        manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDurationUs(lastPeriodIndex));\n    // Get the period-relative start/end times.\n    long currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs;\n    long currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs;\n    if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) {\n      // The manifest describes an incomplete live stream. Update the start/end times to reflect the\n      // live stream duration and the manifest's time shift buffer depth.\n      long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs);\n      long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs\n          - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs);\n      currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs);\n      if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {\n        long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);\n        long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs;\n        int periodIndex = lastPeriodIndex;\n        while (offsetInPeriodUs < 0 && periodIndex > 0) {\n          offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex);\n        }\n        if (periodIndex == 0) {\n          currentStartTimeUs = Math.max(currentStartTimeUs, offsetInPeriodUs);\n        } else {\n          // The time shift buffer starts after the earliest period.\n          // TODO: Does this ever happen?\n          currentStartTimeUs = manifest.getPeriodDurationUs(0);\n        }\n      }\n      windowChangingImplicitly = true;\n    }\n    long windowDurationUs = currentEndTimeUs - currentStartTimeUs;\n    for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {\n      windowDurationUs += manifest.getPeriodDurationUs(i);\n    }\n    long windowDefaultStartPositionUs = 0;\n    if (manifest.dynamic) {\n      long presentationDelayForManifestMs = livePresentationDelayMs;\n      if (!livePresentationDelayOverridesManifest\n          && manifest.suggestedPresentationDelayMs != C.TIME_UNSET) {\n        presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs;\n      }\n      // Snap the default position to the start of the segment containing it.\n      windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs);\n      if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {\n        // The default start position is too close to the start of the live window. Set it to the\n        // minimum default start position provided the window is at least twice as big. Else set\n        // it to the middle of the window.\n        windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US,\n            windowDurationUs / 2);\n      }\n    }\n    long windowStartTimeMs = C.TIME_UNSET;\n    if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {\n      windowStartTimeMs =\n          manifest.availabilityStartTimeMs\n              + manifest.getPeriod(0).startMs\n              + C.usToMs(currentStartTimeUs);\n    }\n    DashTimeline timeline =\n        new DashTimeline(\n            manifest.availabilityStartTimeMs,\n            windowStartTimeMs,\n            firstPeriodId,\n            currentStartTimeUs,\n            windowDurationUs,\n            windowDefaultStartPositionUs,\n            manifest,\n            tag);\n    refreshSourceInfo(timeline);\n\n    if (!sideloadedManifest) {\n      // Remove any pending simulated refresh.\n      handler.removeCallbacks(simulateManifestRefreshRunnable);\n      // If the window is changing implicitly, post a simulated manifest refresh to update it.\n      if (windowChangingImplicitly) {\n        handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS);\n      }\n      if (manifestLoadPending) {\n        startLoadingManifest();\n      } else if (scheduleRefresh\n          && manifest.dynamic\n          && manifest.minUpdatePeriodMs != C.TIME_UNSET) {\n        // Schedule an explicit refresh if needed.\n        long minUpdatePeriodMs = manifest.minUpdatePeriodMs;\n        if (minUpdatePeriodMs == 0) {\n          // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where\n          // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is\n          // explicit signaling in the stream, according to:\n          // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service\n          minUpdatePeriodMs = 5000;\n        }\n        long nextLoadTimestampMs = manifestLoadStartTimestampMs + minUpdatePeriodMs;\n        long delayUntilNextLoadMs =\n            Math.max(0, nextLoadTimestampMs - SystemClock.elapsedRealtime());\n        scheduleManifestRefresh(delayUntilNextLoadMs);\n      }\n    }\n  }\n\n  private void scheduleManifestRefresh(long delayUntilNextLoadMs) {\n    handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);\n  }\n\n  private void startLoadingManifest() {\n    handler.removeCallbacks(refreshManifestRunnable);\n    if (loader.hasFatalError()) {\n      return;\n    }\n    if (loader.isLoading()) {\n      manifestLoadPending = true;\n      return;\n    }\n    Uri manifestUri;\n    synchronized (manifestUriLock) {\n      manifestUri = this.manifestUri;\n    }\n    manifestLoadPending = false;\n    startLoading(\n        new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser),\n        manifestCallback,\n        loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST));\n  }\n\n  private long getManifestLoadRetryDelayMillis() {\n    return Math.min((staleManifestReloadAttempt - 1) * 1000, 5000);\n  }\n\n  private <T> void startLoading(ParsingLoadable<T> loadable,\n      Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {\n    long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);\n    manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);\n  }\n\n  private long getNowUnixTimeUs() {\n    if (elapsedRealtimeOffsetMs != 0) {\n      return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs);\n    } else {\n      return C.msToUs(System.currentTimeMillis());\n    }\n  }\n\n  private static final class PeriodSeekInfo {\n\n    public static PeriodSeekInfo createPeriodSeekInfo(\n        com.google.android.exoplayer2.source.dash.manifest.Period period, long durationUs) {\n      int adaptationSetCount = period.adaptationSets.size();\n      long availableStartTimeUs = 0;\n      long availableEndTimeUs = Long.MAX_VALUE;\n      boolean isIndexExplicit = false;\n      boolean seenEmptyIndex = false;\n\n      boolean haveAudioVideoAdaptationSets = false;\n      for (int i = 0; i < adaptationSetCount; i++) {\n        int type = period.adaptationSets.get(i).type;\n        if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) {\n          haveAudioVideoAdaptationSets = true;\n          break;\n        }\n      }\n\n      for (int i = 0; i < adaptationSetCount; i++) {\n        AdaptationSet adaptationSet = period.adaptationSets.get(i);\n        // Exclude text adaptation sets from duration calculations, if we have at least one audio\n        // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029\n        if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) {\n          continue;\n        }\n\n        DashSegmentIndex index = adaptationSet.representations.get(0).getIndex();\n        if (index == null) {\n          return new PeriodSeekInfo(true, 0, durationUs);\n        }\n        isIndexExplicit |= index.isExplicit();\n        int segmentCount = index.getSegmentCount(durationUs);\n        if (segmentCount == 0) {\n          seenEmptyIndex = true;\n          availableStartTimeUs = 0;\n          availableEndTimeUs = 0;\n        } else if (!seenEmptyIndex) {\n          long firstSegmentNum = index.getFirstSegmentNum();\n          long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum);\n          availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);\n          if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) {\n            long lastSegmentNum = firstSegmentNum + segmentCount - 1;\n            long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum)\n                + index.getDurationUs(lastSegmentNum, durationUs);\n            availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);\n          }\n        }\n      }\n      return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs);\n    }\n\n    public final boolean isIndexExplicit;\n    public final long availableStartTimeUs;\n    public final long availableEndTimeUs;\n\n    private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs,\n        long availableEndTimeUs) {\n      this.isIndexExplicit = isIndexExplicit;\n      this.availableStartTimeUs = availableStartTimeUs;\n      this.availableEndTimeUs = availableEndTimeUs;\n    }\n\n  }\n\n  private static final class DashTimeline extends Timeline {\n\n    private final long presentationStartTimeMs;\n    private final long windowStartTimeMs;\n\n    private final int firstPeriodId;\n    private final long offsetInFirstPeriodUs;\n    private final long windowDurationUs;\n    private final long windowDefaultStartPositionUs;\n    private final DashManifest manifest;\n    @Nullable private final Object windowTag;\n\n    public DashTimeline(\n        long presentationStartTimeMs,\n        long windowStartTimeMs,\n        int firstPeriodId,\n        long offsetInFirstPeriodUs,\n        long windowDurationUs,\n        long windowDefaultStartPositionUs,\n        DashManifest manifest,\n        @Nullable Object windowTag) {\n      this.presentationStartTimeMs = presentationStartTimeMs;\n      this.windowStartTimeMs = windowStartTimeMs;\n      this.firstPeriodId = firstPeriodId;\n      this.offsetInFirstPeriodUs = offsetInFirstPeriodUs;\n      this.windowDurationUs = windowDurationUs;\n      this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;\n      this.manifest = manifest;\n      this.windowTag = windowTag;\n    }\n\n    @Override\n    public int getPeriodCount() {\n      return manifest.getPeriodCount();\n    }\n\n    @Override\n    public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) {\n      Assertions.checkIndex(periodIndex, 0, getPeriodCount());\n      Object id = setIdentifiers ? manifest.getPeriod(periodIndex).id : null;\n      Object uid = setIdentifiers ? (firstPeriodId + periodIndex) : null;\n      return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),\n          C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)\n              - offsetInFirstPeriodUs);\n    }\n\n    @Override\n    public int getWindowCount() {\n      return 1;\n    }\n\n    @Override\n    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {\n      Assertions.checkIndex(windowIndex, 0, 1);\n      long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(\n          defaultPositionProjectionUs);\n      return window.set(\n          Window.SINGLE_WINDOW_UID,\n          windowTag,\n          manifest,\n          presentationStartTimeMs,\n          windowStartTimeMs,\n          /* isSeekable= */ true,\n          /* isDynamic= */ isMovingLiveWindow(manifest),\n          /* isLive= */ manifest.dynamic,\n          windowDefaultStartPositionUs,\n          windowDurationUs,\n          /* firstPeriodIndex= */ 0,\n          /* lastPeriodIndex= */ getPeriodCount() - 1,\n          offsetInFirstPeriodUs);\n    }\n\n    @Override\n    public int getIndexOfPeriod(Object uid) {\n      if (!(uid instanceof Integer)) {\n        return C.INDEX_UNSET;\n      }\n      int periodId = (int) uid;\n      int periodIndex = periodId - firstPeriodId;\n      return periodIndex < 0 || periodIndex >= getPeriodCount() ? C.INDEX_UNSET : periodIndex;\n    }\n\n    private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) {\n      long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;\n      if (!isMovingLiveWindow(manifest)) {\n        return windowDefaultStartPositionUs;\n      }\n      if (defaultPositionProjectionUs > 0) {\n        windowDefaultStartPositionUs += defaultPositionProjectionUs;\n        if (windowDefaultStartPositionUs > windowDurationUs) {\n          // The projection takes us beyond the end of the live window.\n          return C.TIME_UNSET;\n        }\n      }\n      // Attempt to snap to the start of the corresponding video segment.\n      int periodIndex = 0;\n      long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs;\n      long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\n      while (periodIndex < manifest.getPeriodCount() - 1\n          && defaultStartPositionInPeriodUs >= periodDurationUs) {\n        defaultStartPositionInPeriodUs -= periodDurationUs;\n        periodIndex++;\n        periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\n      }\n      com.google.android.exoplayer2.source.dash.manifest.Period period =\n          manifest.getPeriod(periodIndex);\n      int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);\n      if (videoAdaptationSetIndex == C.INDEX_UNSET) {\n        // No video adaptation set for snapping.\n        return windowDefaultStartPositionUs;\n      }\n      // If there are multiple video adaptation sets with unaligned segments, the initial time may\n      // not correspond to the start of a segment in both, but this is an edge case.\n      DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)\n          .representations.get(0).getIndex();\n      if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {\n        // Video adaptation set does not include a non-empty index for snapping.\n        return windowDefaultStartPositionUs;\n      }\n      long segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs);\n      return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum)\n          - defaultStartPositionInPeriodUs;\n    }\n\n    @Override\n    public Object getUidOfPeriod(int periodIndex) {\n      Assertions.checkIndex(periodIndex, 0, getPeriodCount());\n      return firstPeriodId + periodIndex;\n    }\n\n    private static boolean isMovingLiveWindow(DashManifest manifest) {\n      return manifest.dynamic\n          && manifest.minUpdatePeriodMs != C.TIME_UNSET\n          && manifest.durationMs == C.TIME_UNSET;\n    }\n  }\n\n  private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback {\n\n    @Override\n    public void onDashManifestRefreshRequested() {\n      DashMediaSource.this.onDashManifestRefreshRequested();\n    }\n\n    @Override\n    public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {\n      DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);\n    }\n  }\n\n  private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> {\n\n    @Override\n    public void onLoadCompleted(ParsingLoadable<DashManifest> loadable,\n        long elapsedRealtimeMs, long loadDurationMs) {\n      onManifestLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs);\n    }\n\n    @Override\n    public void onLoadCanceled(ParsingLoadable<DashManifest> loadable,\n        long elapsedRealtimeMs, long loadDurationMs, boolean released) {\n      DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs);\n    }\n\n    @Override\n    public LoadErrorAction onLoadError(\n        ParsingLoadable<DashManifest> loadable,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        IOException error,\n        int errorCount) {\n      return onManifestLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error, errorCount);\n    }\n\n  }\n\n  private final class UtcTimestampCallback implements Loader.Callback<ParsingLoadable<Long>> {\n\n    @Override\n    public void onLoadCompleted(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,\n        long loadDurationMs) {\n      onUtcTimestampLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs);\n    }\n\n    @Override\n    public void onLoadCanceled(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,\n        long loadDurationMs, boolean released) {\n      DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs);\n    }\n\n    @Override\n    public LoadErrorAction onLoadError(\n        ParsingLoadable<Long> loadable,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        IOException error,\n        int errorCount) {\n      return onUtcTimestampLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error);\n    }\n\n  }\n\n  private static final class XsDateTimeParser implements ParsingLoadable.Parser<Long> {\n\n    @Override\n    public Long parse(Uri uri, InputStream inputStream) throws IOException {\n      String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();\n      return Util.parseXsDateTime(firstLine);\n    }\n\n  }\n\n  /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser<Long> {\n\n    private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN =\n        Pattern.compile(\"(.+?)(Z|((\\\\+|-|−)(\\\\d\\\\d)(:?(\\\\d\\\\d))?))\");\n\n    @Override\n    public Long parse(Uri uri, InputStream inputStream) throws IOException {\n      String firstLine =\n          new BufferedReader(new InputStreamReader(inputStream, Charset.forName(C.UTF8_NAME)))\n              .readLine();\n      try {\n        Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine);\n        if (!matcher.matches()) {\n          throw new ParserException(\"Couldn't parse timestamp: \" + firstLine);\n        }\n        // Parse the timestamp.\n        String timestampWithoutTimezone = matcher.group(1);\n        SimpleDateFormat format = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss\", Locale.US);\n        format.setTimeZone(TimeZone.getTimeZone(\"UTC\"));\n        long timestampMs = format.parse(timestampWithoutTimezone).getTime();\n        // Parse the timezone.\n        String timezone = matcher.group(2);\n        if (\"Z\".equals(timezone)) {\n          // UTC (no offset).\n        } else {\n          long sign = \"+\".equals(matcher.group(4)) ? 1 : -1;\n          long hours = Long.parseLong(matcher.group(5));\n          String minutesString = matcher.group(7);\n          long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString);\n          long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000);\n          timestampMs -= timestampOffsetMs;\n        }\n        return timestampMs;\n      } catch (ParseException e) {\n        throw new ParserException(e);\n      }\n    }\n\n  }\n\n  /**\n   * A {@link LoaderErrorThrower} that throws fatal {@link IOException} that has occurred during\n   * manifest loading from the manifest {@code loader}, or exception with the loaded manifest.\n   */\n  /* package */ final class ManifestLoadErrorThrower implements LoaderErrorThrower {\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      loader.maybeThrowError();\n      maybeThrowManifestError();\n    }\n\n    @Override\n    public void maybeThrowError(int minRetryCount) throws IOException {\n      loader.maybeThrowError(minRetryCount);\n      maybeThrowManifestError();\n    }\n\n    private void maybeThrowManifestError() throws IOException {\n      if (manifestFatalError != null) {\n        throw manifestFatalError;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\n\n/**\n * Indexes the segments within a media stream.\n */\npublic interface DashSegmentIndex {\n\n  int INDEX_UNBOUNDED = -1;\n\n  /**\n   * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is\n   * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() +\n   * getSegmentCount() - 1} if the given media time is later than the end of the last segment.\n   * Otherwise, returns the segment number of the segment containing the given media time.\n   *\n   * @param timeUs The time in microseconds.\n   * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link\n   *     C#TIME_UNSET} if the period's duration is not yet known.\n   * @return The segment number of the corresponding segment.\n   */\n  long getSegmentNum(long timeUs, long periodDurationUs);\n\n  /**\n   * Returns the start time of a segment.\n   *\n   * @param segmentNum The segment number.\n   * @return The corresponding start time in microseconds.\n   */\n  long getTimeUs(long segmentNum);\n\n  /**\n   * Returns the duration of a segment.\n   *\n   * @param segmentNum The segment number.\n   * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link\n   *     C#TIME_UNSET} if the period's duration is not yet known.\n   * @return The duration of the segment, in microseconds.\n   */\n  long getDurationUs(long segmentNum, long periodDurationUs);\n\n  /**\n   * Returns a {@link RangedUri} defining the location of a segment.\n   *\n   * @param segmentNum The segment number.\n   * @return The {@link RangedUri} defining the location of the data.\n   */\n  RangedUri getSegmentUrl(long segmentNum);\n\n  /**\n   * Returns the segment number of the first segment.\n   *\n   * @return The segment number of the first segment.\n   */\n  long getFirstSegmentNum();\n\n  /**\n   * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}.\n   * <p>\n   * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a\n   * SegmentTimeline element, and if the period duration is not yet known. In this case the caller\n   * must manually determine the window of currently available segments.\n   *\n   * @param periodDurationUs The duration of the enclosing period in microseconds, or\n   *     {@link C#TIME_UNSET} if the period's duration is not yet known.\n   * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}.\n   */\n  int getSegmentCount(long periodDurationUs);\n\n  /**\n   * Returns true if segments are defined explicitly by the index.\n   * <p>\n   * If true is returned, each segment is defined explicitly by the index data, and all of the\n   * listed segments are guaranteed to be available at the time when the index was obtained.\n   * <p>\n   * If false is returned then segment information was derived from properties such as a fixed\n   * segment duration. If the presentation is dynamic, it's possible that only a subset of the\n   * segments are available.\n   *\n   * @return Whether segments are defined explicitly by the index.\n   */\n  boolean isExplicit();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\nimport com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;\nimport com.google.android.exoplayer2.source.chunk.InitializationChunk;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * Utility methods for DASH streams.\n */\npublic final class DashUtil {\n\n  /**\n   * Loads a DASH manifest.\n   *\n   * @param dataSource The {@link HttpDataSource} from which the manifest should be read.\n   * @param uri The {@link Uri} of the manifest to be read.\n   * @return An instance of {@link DashManifest}.\n   * @throws IOException Thrown when there is an error while loading.\n   */\n  public static DashManifest loadManifest(DataSource dataSource, Uri uri)\n      throws IOException {\n    return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST);\n  }\n\n  /**\n   * Loads {@link DrmInitData} for a given period in a DASH manifest.\n   *\n   * @param dataSource The {@link HttpDataSource} from which data should be loaded.\n   * @param period The {@link Period}.\n   * @return The loaded {@link DrmInitData}, or null if none is defined.\n   * @throws IOException Thrown when there is an error while loading.\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   */\n  @Nullable\n  public static DrmInitData loadDrmInitData(DataSource dataSource, Period period)\n      throws IOException, InterruptedException {\n    int primaryTrackType = C.TRACK_TYPE_VIDEO;\n    Representation representation = getFirstRepresentation(period, primaryTrackType);\n    if (representation == null) {\n      primaryTrackType = C.TRACK_TYPE_AUDIO;\n      representation = getFirstRepresentation(period, primaryTrackType);\n      if (representation == null) {\n        return null;\n      }\n    }\n    Format manifestFormat = representation.format;\n    Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation);\n    return sampleFormat == null\n        ? manifestFormat.drmInitData\n        : sampleFormat.copyWithManifestFormatInfo(manifestFormat).drmInitData;\n  }\n\n  /**\n   * Loads initialization data for the {@code representation} and returns the sample {@link Format}.\n   *\n   * @param dataSource The source from which the data should be loaded.\n   * @param trackType The type of the representation. Typically one of the {@link\n   *     com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants.\n   * @param representation The representation which initialization chunk belongs to.\n   * @return the sample {@link Format} of the given representation.\n   * @throws IOException Thrown when there is an error while loading.\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   */\n  @Nullable\n  public static Format loadSampleFormat(\n      DataSource dataSource, int trackType, Representation representation)\n      throws IOException, InterruptedException {\n    ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType,\n        representation, false);\n    return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0];\n  }\n\n  /**\n   * Loads initialization and index data for the {@code representation} and returns the {@link\n   * ChunkIndex}.\n   *\n   * @param dataSource The source from which the data should be loaded.\n   * @param trackType The type of the representation. Typically one of the {@link\n   *     com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants.\n   * @param representation The representation which initialization chunk belongs to.\n   * @return The {@link ChunkIndex} of the given representation, or null if no initialization or\n   *     index data exists.\n   * @throws IOException Thrown when there is an error while loading.\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   */\n  @Nullable\n  public static ChunkIndex loadChunkIndex(\n      DataSource dataSource, int trackType, Representation representation)\n      throws IOException, InterruptedException {\n    ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType,\n        representation, true);\n    return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap();\n  }\n\n  /**\n   * Loads initialization data for the {@code representation} and optionally index data then returns\n   * a {@link ChunkExtractorWrapper} which contains the output.\n   *\n   * @param dataSource The source from which the data should be loaded.\n   * @param trackType The type of the representation. Typically one of the {@link\n   *     com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants.\n   * @param representation The representation which initialization chunk belongs to.\n   * @param loadIndex Whether to load index data too.\n   * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no\n   *     initialization or (if requested) index data exists.\n   * @throws IOException Thrown when there is an error while loading.\n   * @throws InterruptedException Thrown if the thread was interrupted.\n   */\n  @Nullable\n  private static ChunkExtractorWrapper loadInitializationData(\n      DataSource dataSource, int trackType, Representation representation, boolean loadIndex)\n      throws IOException, InterruptedException {\n    RangedUri initializationUri = representation.getInitializationUri();\n    if (initializationUri == null) {\n      return null;\n    }\n    ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(trackType, representation.format);\n    RangedUri requestUri;\n    if (loadIndex) {\n      RangedUri indexUri = representation.getIndexUri();\n      if (indexUri == null) {\n        return null;\n      }\n      // It's common for initialization and index data to be stored adjacently. Attempt to merge\n      // the two requests together to request both at once.\n      requestUri = initializationUri.attemptMerge(indexUri, representation.baseUrl);\n      if (requestUri == null) {\n        loadInitializationData(dataSource, representation, extractorWrapper, initializationUri);\n        requestUri = indexUri;\n      }\n    } else {\n      requestUri = initializationUri;\n    }\n    loadInitializationData(dataSource, representation, extractorWrapper, requestUri);\n    return extractorWrapper;\n  }\n\n  private static void loadInitializationData(DataSource dataSource,\n      Representation representation, ChunkExtractorWrapper extractorWrapper, RangedUri requestUri)\n      throws IOException, InterruptedException {\n    DataSpec dataSpec = new DataSpec(requestUri.resolveUri(representation.baseUrl),\n        requestUri.start, requestUri.length, representation.getCacheKey());\n    InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,\n        representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,\n        extractorWrapper);\n    initializationChunk.load();\n  }\n\n  private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format format) {\n    String mimeType = format.containerMimeType;\n    boolean isWebm =\n        mimeType != null\n            && (mimeType.startsWith(MimeTypes.VIDEO_WEBM)\n                || mimeType.startsWith(MimeTypes.AUDIO_WEBM));\n    Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();\n    return new ChunkExtractorWrapper(extractor, trackType, format);\n  }\n\n  @Nullable\n  private static Representation getFirstRepresentation(Period period, int type) {\n    int index = period.getAdaptationSetIndex(type);\n    if (index == C.INDEX_UNSET) {\n      return null;\n    }\n    List<Representation> representations = period.adaptationSets.get(index).representations;\n    return representations.isEmpty() ? null : representations.get(0);\n  }\n\n  private DashUtil() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\n\n/**\n * An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a\n * media stream.\n */\npublic final class DashWrappingSegmentIndex implements DashSegmentIndex {\n\n  private final ChunkIndex chunkIndex;\n  private final long timeOffsetUs;\n\n  /**\n   * @param chunkIndex The {@link ChunkIndex} to wrap.\n   * @param timeOffsetUs An offset to subtract from the times in the wrapped index, in microseconds.\n   */\n  public DashWrappingSegmentIndex(ChunkIndex chunkIndex, long timeOffsetUs) {\n    this.chunkIndex = chunkIndex;\n    this.timeOffsetUs = timeOffsetUs;\n  }\n\n  @Override\n  public long getFirstSegmentNum() {\n    return 0;\n  }\n\n  @Override\n  public int getSegmentCount(long periodDurationUs) {\n    return chunkIndex.length;\n  }\n\n  @Override\n  public long getTimeUs(long segmentNum) {\n    return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs;\n  }\n\n  @Override\n  public long getDurationUs(long segmentNum, long periodDurationUs) {\n    return chunkIndex.durationsUs[(int) segmentNum];\n  }\n\n  @Override\n  public RangedUri getSegmentUrl(long segmentNum) {\n    return new RangedUri(\n        null, chunkIndex.offsets[(int) segmentNum], chunkIndex.sizes[(int) segmentNum]);\n  }\n\n  @Override\n  public long getSegmentNum(long timeUs, long periodDurationUs) {\n    return chunkIndex.getChunkIndex(timeUs + timeOffsetUs);\n  }\n\n  @Override\n  public boolean isExplicit() {\n    return true;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport android.net.Uri;\nimport android.os.SystemClock;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\nimport com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;\nimport com.google.android.exoplayer2.source.BehindLiveWindowException;\nimport com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;\nimport com.google.android.exoplayer2.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;\nimport com.google.android.exoplayer2.source.chunk.ChunkHolder;\nimport com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;\nimport com.google.android.exoplayer2.source.chunk.InitializationChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;\nimport com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A default {@link DashChunkSource} implementation.\n */\npublic class DefaultDashChunkSource implements DashChunkSource {\n\n  public static final class Factory implements DashChunkSource.Factory {\n\n    private final DataSource.Factory dataSourceFactory;\n    private final int maxSegmentsPerLoad;\n\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this(dataSourceFactory, /* maxSegmentsPerLoad= */ 1);\n    }\n\n    public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) {\n      this.dataSourceFactory = dataSourceFactory;\n      this.maxSegmentsPerLoad = maxSegmentsPerLoad;\n    }\n\n    @Override\n    public DashChunkSource createDashChunkSource(\n        LoaderErrorThrower manifestLoaderErrorThrower,\n        DashManifest manifest,\n        int periodIndex,\n        int[] adaptationSetIndices,\n        TrackSelection trackSelection,\n        int trackType,\n        long elapsedRealtimeOffsetMs,\n        boolean enableEventMessageTrack,\n        List<Format> closedCaptionFormats,\n        @Nullable PlayerTrackEmsgHandler playerEmsgHandler,\n        @Nullable TransferListener transferListener) {\n      DataSource dataSource = dataSourceFactory.createDataSource();\n      if (transferListener != null) {\n        dataSource.addTransferListener(transferListener);\n      }\n      return new DefaultDashChunkSource(\n          manifestLoaderErrorThrower,\n          manifest,\n          periodIndex,\n          adaptationSetIndices,\n          trackSelection,\n          trackType,\n          dataSource,\n          elapsedRealtimeOffsetMs,\n          maxSegmentsPerLoad,\n          enableEventMessageTrack,\n          closedCaptionFormats,\n          playerEmsgHandler);\n    }\n\n  }\n\n  private final LoaderErrorThrower manifestLoaderErrorThrower;\n  private final int[] adaptationSetIndices;\n  private final int trackType;\n  private final DataSource dataSource;\n  private final long elapsedRealtimeOffsetMs;\n  private final int maxSegmentsPerLoad;\n  @Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler;\n\n  protected final RepresentationHolder[] representationHolders;\n\n  private TrackSelection trackSelection;\n  private DashManifest manifest;\n  private int periodIndex;\n  private IOException fatalError;\n  private boolean missingLastSegment;\n  private long liveEdgeTimeUs;\n\n  /**\n   * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.\n   * @param manifest The initial manifest.\n   * @param periodIndex The index of the period in the manifest.\n   * @param adaptationSetIndices The indices of the adaptation sets in the period.\n   * @param trackSelection The track selection.\n   * @param trackType The type of the tracks in the selection.\n   * @param dataSource A {@link DataSource} suitable for loading the media data.\n   * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between\n   *     server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified\n   *     as the server's unix time minus the local elapsed time. If unknown, set to 0.\n   * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note\n   *     that segments will only be combined if their {@link Uri}s are the same and if their data\n   *     ranges are adjacent.\n   * @param enableEventMessageTrack Whether to output an event message track.\n   * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output.\n   * @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg\n   *     messages targeting the player. Maybe null if this is not necessary.\n   */\n  public DefaultDashChunkSource(\n      LoaderErrorThrower manifestLoaderErrorThrower,\n      DashManifest manifest,\n      int periodIndex,\n      int[] adaptationSetIndices,\n      TrackSelection trackSelection,\n      int trackType,\n      DataSource dataSource,\n      long elapsedRealtimeOffsetMs,\n      int maxSegmentsPerLoad,\n      boolean enableEventMessageTrack,\n      List<Format> closedCaptionFormats,\n      @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) {\n    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\n    this.manifest = manifest;\n    this.adaptationSetIndices = adaptationSetIndices;\n    this.trackSelection = trackSelection;\n    this.trackType = trackType;\n    this.dataSource = dataSource;\n    this.periodIndex = periodIndex;\n    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;\n    this.maxSegmentsPerLoad = maxSegmentsPerLoad;\n    this.playerTrackEmsgHandler = playerTrackEmsgHandler;\n\n    long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\n    liveEdgeTimeUs = C.TIME_UNSET;\n\n    List<Representation> representations = getRepresentations();\n    representationHolders = new RepresentationHolder[trackSelection.length()];\n    for (int i = 0; i < representationHolders.length; i++) {\n      Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));\n      representationHolders[i] =\n          new RepresentationHolder(\n              periodDurationUs,\n              trackType,\n              representation,\n              enableEventMessageTrack,\n              closedCaptionFormats,\n              playerTrackEmsgHandler);\n    }\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    // Segments are aligned across representations, so any segment index will do.\n    for (RepresentationHolder representationHolder : representationHolders) {\n      if (representationHolder.segmentIndex != null) {\n        long segmentNum = representationHolder.getSegmentNum(positionUs);\n        long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);\n        long secondSyncUs =\n            firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1\n                ? representationHolder.getSegmentStartTimeUs(segmentNum + 1)\n                : firstSyncUs;\n        return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);\n      }\n    }\n    // We don't have a segment index to adjust the seek position with yet.\n    return positionUs;\n  }\n\n  @Override\n  public void updateManifest(DashManifest newManifest, int newPeriodIndex) {\n    try {\n      manifest = newManifest;\n      periodIndex = newPeriodIndex;\n      long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\n      List<Representation> representations = getRepresentations();\n      for (int i = 0; i < representationHolders.length; i++) {\n        Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));\n        representationHolders[i] =\n            representationHolders[i].copyWithNewRepresentation(periodDurationUs, representation);\n      }\n    } catch (BehindLiveWindowException e) {\n      fatalError = e;\n    }\n  }\n\n  @Override\n  public void updateTrackSelection(TrackSelection trackSelection) {\n    this.trackSelection = trackSelection;\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    if (fatalError != null) {\n      throw fatalError;\n    } else {\n      manifestLoaderErrorThrower.maybeThrowError();\n    }\n  }\n\n  @Override\n  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\n    if (fatalError != null || trackSelection.length() < 2) {\n      return queue.size();\n    }\n    return trackSelection.evaluateQueueSize(playbackPositionUs, queue);\n  }\n\n  @Override\n  public void getNextChunk(\n      long playbackPositionUs,\n      long loadPositionUs,\n      List<? extends MediaChunk> queue,\n      ChunkHolder out) {\n    if (fatalError != null) {\n      return;\n    }\n\n    long bufferedDurationUs = loadPositionUs - playbackPositionUs;\n    long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);\n    long presentationPositionUs =\n        C.msToUs(manifest.availabilityStartTimeMs)\n            + C.msToUs(manifest.getPeriod(periodIndex).startMs)\n            + loadPositionUs;\n\n    if (playerTrackEmsgHandler != null\n        && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(\n            presentationPositionUs)) {\n      return;\n    }\n\n    long nowUnixTimeUs = getNowUnixTimeUs();\n    MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);\n    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];\n    for (int i = 0; i < chunkIterators.length; i++) {\n      RepresentationHolder representationHolder = representationHolders[i];\n      if (representationHolder.segmentIndex == null) {\n        chunkIterators[i] = MediaChunkIterator.EMPTY;\n      } else {\n        long firstAvailableSegmentNum =\n            representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\n        long lastAvailableSegmentNum =\n            representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\n        long segmentNum =\n            getSegmentNum(\n                representationHolder,\n                previous,\n                loadPositionUs,\n                firstAvailableSegmentNum,\n                lastAvailableSegmentNum);\n        if (segmentNum < firstAvailableSegmentNum) {\n          chunkIterators[i] = MediaChunkIterator.EMPTY;\n        } else {\n          chunkIterators[i] =\n              new RepresentationSegmentIterator(\n                  representationHolder, segmentNum, lastAvailableSegmentNum);\n        }\n      }\n    }\n\n    trackSelection.updateSelectedTrack(\n        playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);\n\n    RepresentationHolder representationHolder =\n        representationHolders[trackSelection.getSelectedIndex()];\n\n    if (representationHolder.extractorWrapper != null) {\n      Representation selectedRepresentation = representationHolder.representation;\n      RangedUri pendingInitializationUri = null;\n      RangedUri pendingIndexUri = null;\n      if (representationHolder.extractorWrapper.getSampleFormats() == null) {\n        pendingInitializationUri = selectedRepresentation.getInitializationUri();\n      }\n      if (representationHolder.segmentIndex == null) {\n        pendingIndexUri = selectedRepresentation.getIndexUri();\n      }\n      if (pendingInitializationUri != null || pendingIndexUri != null) {\n        // We have initialization and/or index requests to make.\n        out.chunk = newInitializationChunk(representationHolder, dataSource,\n            trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),\n            trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);\n        return;\n      }\n    }\n\n    long periodDurationUs = representationHolder.periodDurationUs;\n    boolean periodEnded = periodDurationUs != C.TIME_UNSET;\n\n    if (representationHolder.getSegmentCount() == 0) {\n      // The index doesn't define any segments.\n      out.endOfStream = periodEnded;\n      return;\n    }\n\n    long firstAvailableSegmentNum =\n        representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\n    long lastAvailableSegmentNum =\n        representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\n\n    updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum);\n\n    long segmentNum =\n        getSegmentNum(\n            representationHolder,\n            previous,\n            loadPositionUs,\n            firstAvailableSegmentNum,\n            lastAvailableSegmentNum);\n    if (segmentNum < firstAvailableSegmentNum) {\n      // This is before the first chunk in the current manifest.\n      fatalError = new BehindLiveWindowException();\n      return;\n    }\n\n    if (segmentNum > lastAvailableSegmentNum\n        || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {\n      // The segment is beyond the end of the period.\n      out.endOfStream = periodEnded;\n      return;\n    }\n\n    if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {\n      // The period duration clips the period to a position before the segment.\n      out.endOfStream = true;\n      return;\n    }\n\n    int maxSegmentCount =\n        (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);\n    if (periodDurationUs != C.TIME_UNSET) {\n      while (maxSegmentCount > 1\n          && representationHolder.getSegmentStartTimeUs(segmentNum + maxSegmentCount - 1)\n              >= periodDurationUs) {\n        // The period duration clips the period to a position before the last segment in the range\n        // [segmentNum, segmentNum + maxSegmentCount - 1]. Reduce maxSegmentCount.\n        maxSegmentCount--;\n      }\n    }\n\n    long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;\n    out.chunk =\n        newMediaChunk(\n            representationHolder,\n            dataSource,\n            trackType,\n            trackSelection.getSelectedFormat(),\n            trackSelection.getSelectionReason(),\n            trackSelection.getSelectionData(),\n            segmentNum,\n            maxSegmentCount,\n            seekTimeUs);\n  }\n\n  @Override\n  public void onChunkLoadCompleted(Chunk chunk) {\n    if (chunk instanceof InitializationChunk) {\n      InitializationChunk initializationChunk = (InitializationChunk) chunk;\n      int trackIndex = trackSelection.indexOf(initializationChunk.trackFormat);\n      RepresentationHolder representationHolder = representationHolders[trackIndex];\n      // The null check avoids overwriting an index obtained from the manifest with one obtained\n      // from the stream. If the manifest defines an index then the stream shouldn't, but in cases\n      // where it does we should ignore it.\n      if (representationHolder.segmentIndex == null) {\n        SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap();\n        if (seekMap != null) {\n          representationHolders[trackIndex] =\n              representationHolder.copyWithNewSegmentIndex(\n                  new DashWrappingSegmentIndex(\n                      (ChunkIndex) seekMap,\n                      representationHolder.representation.presentationTimeOffsetUs));\n        }\n      }\n    }\n    if (playerTrackEmsgHandler != null) {\n      playerTrackEmsgHandler.onChunkLoadCompleted(chunk);\n    }\n  }\n\n  @Override\n  public boolean onChunkLoadError(\n      Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) {\n    if (!cancelable) {\n      return false;\n    }\n    if (playerTrackEmsgHandler != null\n        && playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) {\n      return true;\n    }\n    // Workaround for missing segment at the end of the period\n    if (!manifest.dynamic && chunk instanceof MediaChunk\n        && e instanceof InvalidResponseCodeException\n        && ((InvalidResponseCodeException) e).responseCode == 404) {\n      RepresentationHolder representationHolder =\n          representationHolders[trackSelection.indexOf(chunk.trackFormat)];\n      int segmentCount = representationHolder.getSegmentCount();\n      if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) {\n        long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1;\n        if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {\n          missingLastSegment = true;\n          return true;\n        }\n      }\n    }\n    return blacklistDurationMs != C.TIME_UNSET\n        && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), blacklistDurationMs);\n  }\n\n  // Internal methods.\n\n  private long getSegmentNum(\n      RepresentationHolder representationHolder,\n      @Nullable MediaChunk previousChunk,\n      long loadPositionUs,\n      long firstAvailableSegmentNum,\n      long lastAvailableSegmentNum) {\n    return previousChunk != null\n        ? previousChunk.getNextChunkIndex()\n        : Util.constrainValue(\n            representationHolder.getSegmentNum(loadPositionUs),\n            firstAvailableSegmentNum,\n            lastAvailableSegmentNum);\n  }\n\n  private ArrayList<Representation> getRepresentations() {\n    List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;\n    ArrayList<Representation> representations = new ArrayList<>();\n    for (int adaptationSetIndex : adaptationSetIndices) {\n      representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations);\n    }\n    return representations;\n  }\n\n  private void updateLiveEdgeTimeUs(\n      RepresentationHolder representationHolder, long lastAvailableSegmentNum) {\n    liveEdgeTimeUs = manifest.dynamic\n        ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET;\n  }\n\n  private long getNowUnixTimeUs() {\n    if (elapsedRealtimeOffsetMs != 0) {\n      return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000;\n    } else {\n      return System.currentTimeMillis() * 1000;\n    }\n  }\n\n  private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {\n    boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET;\n    return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET;\n  }\n\n  protected Chunk newInitializationChunk(\n      RepresentationHolder representationHolder,\n      DataSource dataSource,\n      Format trackFormat,\n      int trackSelectionReason,\n      Object trackSelectionData,\n      RangedUri initializationUri,\n      RangedUri indexUri) {\n    RangedUri requestUri;\n    String baseUrl = representationHolder.representation.baseUrl;\n    if (initializationUri != null) {\n      // It's common for initialization and index data to be stored adjacently. Attempt to merge\n      // the two requests together to request both at once.\n      requestUri = initializationUri.attemptMerge(indexUri, baseUrl);\n      if (requestUri == null) {\n        requestUri = initializationUri;\n      }\n    } else {\n      requestUri = indexUri;\n    }\n    DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,\n        requestUri.length, representationHolder.representation.getCacheKey());\n    return new InitializationChunk(dataSource, dataSpec, trackFormat,\n        trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);\n  }\n\n  protected Chunk newMediaChunk(\n      RepresentationHolder representationHolder,\n      DataSource dataSource,\n      int trackType,\n      Format trackFormat,\n      int trackSelectionReason,\n      Object trackSelectionData,\n      long firstSegmentNum,\n      int maxSegmentCount,\n      long seekTimeUs) {\n    Representation representation = representationHolder.representation;\n    long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);\n    RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);\n    String baseUrl = representation.baseUrl;\n    if (representationHolder.extractorWrapper == null) {\n      long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);\n      DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),\n          segmentUri.start, segmentUri.length, representation.getCacheKey());\n      return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,\n          trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat);\n    } else {\n      int segmentCount = 1;\n      for (int i = 1; i < maxSegmentCount; i++) {\n        RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i);\n        RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl);\n        if (mergedSegmentUri == null) {\n          // Unable to merge segment fetches because the URIs do not merge.\n          break;\n        }\n        segmentUri = mergedSegmentUri;\n        segmentCount++;\n      }\n      long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1);\n      long periodDurationUs = representationHolder.periodDurationUs;\n      long clippedEndTimeUs =\n          periodDurationUs != C.TIME_UNSET && periodDurationUs <= endTimeUs\n              ? periodDurationUs\n              : C.TIME_UNSET;\n      DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),\n          segmentUri.start, segmentUri.length, representation.getCacheKey());\n      long sampleOffsetUs = -representation.presentationTimeOffsetUs;\n      return new ContainerMediaChunk(\n          dataSource,\n          dataSpec,\n          trackFormat,\n          trackSelectionReason,\n          trackSelectionData,\n          startTimeUs,\n          endTimeUs,\n          seekTimeUs,\n          clippedEndTimeUs,\n          firstSegmentNum,\n          segmentCount,\n          sampleOffsetUs,\n          representationHolder.extractorWrapper);\n    }\n  }\n\n  // Protected classes.\n\n  /** {@link MediaChunkIterator} wrapping a {@link RepresentationHolder}. */\n  protected static final class RepresentationSegmentIterator extends BaseMediaChunkIterator {\n\n    private final RepresentationHolder representationHolder;\n\n    /**\n     * Creates iterator.\n     *\n     * @param representation The {@link RepresentationHolder} to wrap.\n     * @param firstAvailableSegmentNum The number of the first available segment.\n     * @param lastAvailableSegmentNum The number of the last available segment.\n     */\n    public RepresentationSegmentIterator(\n        RepresentationHolder representation,\n        long firstAvailableSegmentNum,\n        long lastAvailableSegmentNum) {\n      super(/* fromIndex= */ firstAvailableSegmentNum, /* toIndex= */ lastAvailableSegmentNum);\n      this.representationHolder = representation;\n    }\n\n    @Override\n    public DataSpec getDataSpec() {\n      checkInBounds();\n      Representation representation = representationHolder.representation;\n      RangedUri segmentUri = representationHolder.getSegmentUrl(getCurrentIndex());\n      Uri resolvedUri = segmentUri.resolveUri(representation.baseUrl);\n      String cacheKey = representation.getCacheKey();\n      return new DataSpec(resolvedUri, segmentUri.start, segmentUri.length, cacheKey);\n    }\n\n    @Override\n    public long getChunkStartTimeUs() {\n      checkInBounds();\n      return representationHolder.getSegmentStartTimeUs(getCurrentIndex());\n    }\n\n    @Override\n    public long getChunkEndTimeUs() {\n      checkInBounds();\n      return representationHolder.getSegmentEndTimeUs(getCurrentIndex());\n    }\n  }\n\n  /** Holds information about a snapshot of a single {@link Representation}. */\n  protected static final class RepresentationHolder {\n\n    /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper;\n\n    public final Representation representation;\n    @Nullable public final DashSegmentIndex segmentIndex;\n\n    private final long periodDurationUs;\n    private final long segmentNumShift;\n\n    /* package */ RepresentationHolder(\n        long periodDurationUs,\n        int trackType,\n        Representation representation,\n        boolean enableEventMessageTrack,\n        List<Format> closedCaptionFormats,\n        @Nullable TrackOutput playerEmsgTrackOutput) {\n      this(\n          periodDurationUs,\n          representation,\n          createExtractorWrapper(\n              trackType,\n              representation,\n              enableEventMessageTrack,\n              closedCaptionFormats,\n              playerEmsgTrackOutput),\n          /* segmentNumShift= */ 0,\n          representation.getIndex());\n    }\n\n    private RepresentationHolder(\n        long periodDurationUs,\n        Representation representation,\n        @Nullable ChunkExtractorWrapper extractorWrapper,\n        long segmentNumShift,\n        @Nullable DashSegmentIndex segmentIndex) {\n      this.periodDurationUs = periodDurationUs;\n      this.representation = representation;\n      this.segmentNumShift = segmentNumShift;\n      this.extractorWrapper = extractorWrapper;\n      this.segmentIndex = segmentIndex;\n    }\n\n    @CheckResult\n    /* package */ RepresentationHolder copyWithNewRepresentation(\n        long newPeriodDurationUs, Representation newRepresentation)\n        throws BehindLiveWindowException {\n      DashSegmentIndex oldIndex = representation.getIndex();\n      DashSegmentIndex newIndex = newRepresentation.getIndex();\n\n      if (oldIndex == null) {\n        // Segment numbers cannot shift if the index isn't defined by the manifest.\n        return new RepresentationHolder(\n            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, oldIndex);\n      }\n\n      if (!oldIndex.isExplicit()) {\n        // Segment numbers cannot shift if the index isn't explicit.\n        return new RepresentationHolder(\n            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex);\n      }\n\n      int oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs);\n      if (oldIndexSegmentCount == 0) {\n        // Segment numbers cannot shift if the old index was empty.\n        return new RepresentationHolder(\n            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex);\n      }\n\n      long oldIndexFirstSegmentNum = oldIndex.getFirstSegmentNum();\n      long oldIndexStartTimeUs = oldIndex.getTimeUs(oldIndexFirstSegmentNum);\n      long oldIndexLastSegmentNum = oldIndexFirstSegmentNum + oldIndexSegmentCount - 1;\n      long oldIndexEndTimeUs =\n          oldIndex.getTimeUs(oldIndexLastSegmentNum)\n              + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs);\n      long newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();\n      long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);\n      long newSegmentNumShift = segmentNumShift;\n      if (oldIndexEndTimeUs == newIndexStartTimeUs) {\n        // The new index continues where the old one ended, with no overlap.\n        newSegmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum;\n      } else if (oldIndexEndTimeUs < newIndexStartTimeUs) {\n        // There's a gap between the old index and the new one which means we've slipped behind the\n        // live window and can't proceed.\n        throw new BehindLiveWindowException();\n      } else if (newIndexStartTimeUs < oldIndexStartTimeUs) {\n        // The new index overlaps with (but does not have a start position contained within) the old\n        // index. This can only happen if extra segments have been added to the start of the index.\n        newSegmentNumShift -=\n            newIndex.getSegmentNum(oldIndexStartTimeUs, newPeriodDurationUs)\n                - oldIndexFirstSegmentNum;\n      } else {\n        // The new index overlaps with (and has a start position contained within) the old index.\n        newSegmentNumShift +=\n            oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs)\n                - newIndexFirstSegmentNum;\n      }\n      return new RepresentationHolder(\n          newPeriodDurationUs, newRepresentation, extractorWrapper, newSegmentNumShift, newIndex);\n    }\n\n    @CheckResult\n    /* package */ RepresentationHolder copyWithNewSegmentIndex(DashSegmentIndex segmentIndex) {\n      return new RepresentationHolder(\n          periodDurationUs, representation, extractorWrapper, segmentNumShift, segmentIndex);\n    }\n\n    public long getFirstSegmentNum() {\n      return segmentIndex.getFirstSegmentNum() + segmentNumShift;\n    }\n\n    public int getSegmentCount() {\n      return segmentIndex.getSegmentCount(periodDurationUs);\n    }\n\n    public long getSegmentStartTimeUs(long segmentNum) {\n      return segmentIndex.getTimeUs(segmentNum - segmentNumShift);\n    }\n\n    public long getSegmentEndTimeUs(long segmentNum) {\n      return getSegmentStartTimeUs(segmentNum)\n          + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs);\n    }\n\n    public long getSegmentNum(long positionUs) {\n      return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift;\n    }\n\n    public RangedUri getSegmentUrl(long segmentNum) {\n      return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);\n    }\n\n    public long getFirstAvailableSegmentNum(\n        DashManifest manifest, int periodIndex, long nowUnixTimeUs) {\n      if (getSegmentCount() == DashSegmentIndex.INDEX_UNBOUNDED\n          && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {\n        // The index is itself unbounded. We need to use the current time to calculate the range of\n        // available segments.\n        long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);\n        long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);\n        long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;\n        long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);\n        return Math.max(\n            getFirstSegmentNum(), getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));\n      }\n      return getFirstSegmentNum();\n    }\n\n    public long getLastAvailableSegmentNum(\n        DashManifest manifest, int periodIndex, long nowUnixTimeUs) {\n      int availableSegmentCount = getSegmentCount();\n      if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {\n        // The index is itself unbounded. We need to use the current time to calculate the range of\n        // available segments.\n        long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);\n        long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);\n        long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;\n        // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get\n        // the index of the last completed segment.\n        return getSegmentNum(liveEdgeTimeInPeriodUs) - 1;\n      }\n      return getFirstSegmentNum() + availableSegmentCount - 1;\n    }\n\n    private static boolean mimeTypeIsWebm(String mimeType) {\n      return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM)\n          || mimeType.startsWith(MimeTypes.APPLICATION_WEBM);\n    }\n\n    private static boolean mimeTypeIsRawText(String mimeType) {\n      return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);\n    }\n\n    private static @Nullable ChunkExtractorWrapper createExtractorWrapper(\n        int trackType,\n        Representation representation,\n        boolean enableEventMessageTrack,\n        List<Format> closedCaptionFormats,\n        @Nullable TrackOutput playerEmsgTrackOutput) {\n      String containerMimeType = representation.format.containerMimeType;\n      if (mimeTypeIsRawText(containerMimeType)) {\n        return null;\n      }\n      Extractor extractor;\n      if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\n        extractor = new RawCcExtractor(representation.format);\n      } else if (mimeTypeIsWebm(containerMimeType)) {\n        extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);\n      } else {\n        int flags = 0;\n        if (enableEventMessageTrack) {\n          flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;\n        }\n        extractor =\n            new FragmentedMp4Extractor(\n                flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput);\n      }\n      // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,\n      // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.\n      return new ChunkExtractorWrapper(extractor, trackType, representation.format);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.dash.manifest.EventStream;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\n\n/**\n * A {@link SampleStream} consisting of serialized {@link EventMessage}s read from an\n * {@link EventStream}.\n */\n/* package */ final class EventSampleStream implements SampleStream {\n\n  private final Format upstreamFormat;\n  private final EventMessageEncoder eventMessageEncoder;\n\n  private long[] eventTimesUs;\n  private boolean eventStreamAppendable;\n  private EventStream eventStream;\n\n  private boolean isFormatSentDownstream;\n  private int currentIndex;\n  private long pendingSeekPositionUs;\n\n  public EventSampleStream(\n      EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) {\n    this.upstreamFormat = upstreamFormat;\n    this.eventStream = eventStream;\n    eventMessageEncoder = new EventMessageEncoder();\n    pendingSeekPositionUs = C.TIME_UNSET;\n    eventTimesUs = eventStream.presentationTimesUs;\n    updateEventStream(eventStream, eventStreamAppendable);\n  }\n\n  public String eventStreamId() {\n    return eventStream.id();\n  }\n\n  public void updateEventStream(EventStream eventStream, boolean eventStreamAppendable) {\n    long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1];\n\n    this.eventStreamAppendable = eventStreamAppendable;\n    this.eventStream = eventStream;\n    this.eventTimesUs = eventStream.presentationTimesUs;\n    if (pendingSeekPositionUs != C.TIME_UNSET) {\n      seekToUs(pendingSeekPositionUs);\n    } else if (lastReadPositionUs != C.TIME_UNSET) {\n      currentIndex =\n          Util.binarySearchCeil(\n              eventTimesUs, lastReadPositionUs, /* inclusive= */ false, /* stayInBounds= */ false);\n    }\n  }\n\n  /**\n   * Seeks to the specified position in microseconds.\n   *\n   * @param positionUs The seek position in microseconds.\n   */\n  public void seekToUs(long positionUs) {\n    currentIndex =\n        Util.binarySearchCeil(\n            eventTimesUs, positionUs, /* inclusive= */ true, /* stayInBounds= */ false);\n    boolean isPendingSeek = eventStreamAppendable && currentIndex == eventTimesUs.length;\n    pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;\n  }\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,\n      boolean formatRequired) {\n    if (formatRequired || !isFormatSentDownstream) {\n      formatHolder.format = upstreamFormat;\n      isFormatSentDownstream = true;\n      return C.RESULT_FORMAT_READ;\n    }\n    if (currentIndex == eventTimesUs.length) {\n      if (!eventStreamAppendable) {\n        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n        return C.RESULT_BUFFER_READ;\n      } else {\n        return C.RESULT_NOTHING_READ;\n      }\n    }\n    int sampleIndex = currentIndex++;\n    byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex]);\n    if (serializedEvent != null) {\n      buffer.ensureSpaceForWrite(serializedEvent.length);\n      buffer.data.put(serializedEvent);\n      buffer.timeUs = eventTimesUs[sampleIndex];\n      buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME);\n      return C.RESULT_BUFFER_READ;\n    } else {\n      return C.RESULT_NOTHING_READ;\n    }\n  }\n\n  @Override\n  public int skipData(long positionUs) {\n    int newIndex =\n        Math.max(currentIndex, Util.binarySearchCeil(eventTimesUs, positionUs, true, false));\n    int skipped = newIndex - currentIndex;\n    currentIndex = newIndex;\n    return skipped;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash;\n\nimport static com.google.android.exoplayer2.util.Util.parseXsDateTime;\n\nimport android.os.Handler;\nimport android.os.Message;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;\nimport com.google.android.exoplayer2.source.SampleQueue;\nimport com.google.android.exoplayer2.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.TreeMap;\n\n/**\n * Handles all emsg messages from all media tracks for the player.\n *\n * <p>This class will only respond to emsg messages which have schemeIdUri\n * \"urn:mpeg:dash:event:2012\", and value \"1\"/\"2\"/\"3\". When it encounters one of these messages, it\n * will handle the message according to Section 4.5.2.1 DASH -IF IOP Version 4.1:\n *\n * <ul>\n *   <li>If both presentation time delta and event duration are zero, it means the media\n *       presentation has ended.\n *   <li>Else, it will parse the message data from the emsg message to find the publishTime of the\n *       expired manifest, and mark manifest with publishTime smaller than that values to be\n *       expired.\n * </ul>\n *\n * In both cases, the DASH media source will be notified, and a manifest reload should be triggered.\n */\npublic final class PlayerEmsgHandler implements Handler.Callback {\n\n  private static final int EMSG_MANIFEST_EXPIRED = 1;\n\n  /** Callbacks for player emsg events encountered during DASH live stream. */\n  public interface PlayerEmsgCallback {\n\n    /** Called when the current manifest should be refreshed. */\n    void onDashManifestRefreshRequested();\n\n    /**\n     * Called when the manifest with the publish time has been expired.\n     *\n     * @param expiredManifestPublishTimeUs The manifest publish time that has been expired.\n     */\n    void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs);\n  }\n\n  private final Allocator allocator;\n  private final PlayerEmsgCallback playerEmsgCallback;\n  private final EventMessageDecoder decoder;\n  private final Handler handler;\n  private final TreeMap<Long, Long> manifestPublishTimeToExpiryTimeUs;\n\n  private DashManifest manifest;\n\n  private long expiredManifestPublishTimeUs;\n  private long lastLoadedChunkEndTimeUs;\n  private long lastLoadedChunkEndTimeBeforeRefreshUs;\n  private boolean isWaitingForManifestRefresh;\n  private boolean released;\n\n  /**\n   * @param manifest The initial manifest.\n   * @param playerEmsgCallback The callback that this event handler can invoke when handling emsg\n   *     messages that generate DASH media source events.\n   * @param allocator An {@link Allocator} from which allocations can be obtained.\n   */\n  public PlayerEmsgHandler(\n      DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) {\n    this.manifest = manifest;\n    this.playerEmsgCallback = playerEmsgCallback;\n    this.allocator = allocator;\n\n    manifestPublishTimeToExpiryTimeUs = new TreeMap<>();\n    handler = Util.createHandler(/* callback= */ this);\n    decoder = new EventMessageDecoder();\n    lastLoadedChunkEndTimeUs = C.TIME_UNSET;\n    lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET;\n  }\n\n  /**\n   * Updates the {@link DashManifest} that this handler works on.\n   *\n   * @param newManifest The updated manifest.\n   */\n  public void updateManifest(DashManifest newManifest) {\n    isWaitingForManifestRefresh = false;\n    expiredManifestPublishTimeUs = C.TIME_UNSET;\n    this.manifest = newManifest;\n    removePreviouslyExpiredManifestPublishTimeValues();\n  }\n\n  /* package */ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {\n    if (!manifest.dynamic) {\n      return false;\n    }\n    if (isWaitingForManifestRefresh) {\n      return true;\n    }\n    boolean manifestRefreshNeeded = false;\n    // Find the smallest publishTime (greater than or equal to the current manifest's publish time)\n    // that has a corresponding expiry time.\n    Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);\n    if (expiredEntry != null) {\n      long expiredPointUs = expiredEntry.getValue();\n      if (expiredPointUs < presentationPositionUs) {\n        expiredManifestPublishTimeUs = expiredEntry.getKey();\n        notifyManifestPublishTimeExpired();\n        manifestRefreshNeeded = true;\n      }\n    }\n    if (manifestRefreshNeeded) {\n      maybeNotifyDashManifestRefreshNeeded();\n    }\n    return manifestRefreshNeeded;\n  }\n\n  /**\n   * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that\n   * signals end-of-stream or Manifest expiry, which results in load error. In this case, we should\n   * notify the Dash media source to refresh its manifest.\n   *\n   * @param chunk The chunk whose load encountered the error.\n   * @return True if manifest refresh has been requested, false otherwise.\n   */\n  /* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {\n    if (!manifest.dynamic) {\n      return false;\n    }\n    if (isWaitingForManifestRefresh) {\n      return true;\n    }\n    boolean isAfterForwardSeek =\n        lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs;\n    if (isAfterForwardSeek) {\n      // if we are after a forward seek, and the playback is dynamic with embedded emsg stream,\n      // there's a chance that we have seek over the emsg messages, in which case we should ask\n      // media source for a refresh.\n      maybeNotifyDashManifestRefreshNeeded();\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * Called when the a new chunk in the current media stream has been loaded.\n   *\n   * @param chunk The chunk whose load has been completed.\n   */\n  /* package */ void onChunkLoadCompleted(Chunk chunk) {\n    if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) {\n      lastLoadedChunkEndTimeUs = chunk.endTimeUs;\n    }\n  }\n\n  /**\n   * Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the\n   * player.\n   */\n  public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) {\n    return \"urn:mpeg:dash:event:2012\".equals(schemeIdUri)\n        && (\"1\".equals(value) || \"2\".equals(value) || \"3\".equals(value));\n  }\n\n  /** Returns a {@link TrackOutput} that emsg messages could be written to. */\n  public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() {\n    return new PlayerTrackEmsgHandler(\n        new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()));\n  }\n\n  /** Release this emsg handler. It should not be reused after this call. */\n  public void release() {\n    released = true;\n    handler.removeCallbacksAndMessages(null);\n  }\n\n  @Override\n  public boolean handleMessage(Message message) {\n    if (released) {\n      return true;\n    }\n    switch (message.what) {\n      case (EMSG_MANIFEST_EXPIRED):\n        ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj;\n        handleManifestExpiredMessage(\n            messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg);\n        return true;\n      default:\n        // Do nothing.\n    }\n    return false;\n  }\n\n  // Internal methods.\n\n  private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) {\n    Long previousExpiryTimeUs = manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg);\n    if (previousExpiryTimeUs == null) {\n      manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs);\n    } else {\n      if (previousExpiryTimeUs > eventTimeUs) {\n        manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs);\n      }\n    }\n  }\n\n  private @Nullable Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) {\n    return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);\n  }\n\n  private void removePreviouslyExpiredManifestPublishTimeValues() {\n    for (Iterator<Map.Entry<Long, Long>> it =\n            manifestPublishTimeToExpiryTimeUs.entrySet().iterator();\n        it.hasNext(); ) {\n      Map.Entry<Long, Long> entry = it.next();\n      long expiredManifestPublishTime = entry.getKey();\n      if (expiredManifestPublishTime < manifest.publishTimeMs) {\n        it.remove();\n      }\n    }\n  }\n\n  private void notifyManifestPublishTimeExpired() {\n    playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);\n  }\n\n  /** Requests DASH media manifest to be refreshed if necessary. */\n  private void maybeNotifyDashManifestRefreshNeeded() {\n    if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET\n        && lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) {\n      // Already requested manifest refresh.\n      return;\n    }\n    isWaitingForManifestRefresh = true;\n    lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs;\n    playerEmsgCallback.onDashManifestRefreshRequested();\n  }\n\n  private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) {\n    try {\n      return parseXsDateTime(Util.fromUtf8Bytes(eventMessage.messageData));\n    } catch (ParserException ignored) {\n      // if we can't parse this event, ignore\n      return C.TIME_UNSET;\n    }\n  }\n\n  /** Handles emsg messages for a specific track for the player. */\n  public final class PlayerTrackEmsgHandler implements TrackOutput {\n\n    private final SampleQueue sampleQueue;\n    private final FormatHolder formatHolder;\n    private final MetadataInputBuffer buffer;\n\n    /* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) {\n      this.sampleQueue = sampleQueue;\n\n      formatHolder = new FormatHolder();\n      buffer = new MetadataInputBuffer();\n    }\n\n    @Override\n    public void format(Format format) {\n      sampleQueue.format(format);\n    }\n\n    @Override\n    public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n        throws IOException, InterruptedException {\n      return sampleQueue.sampleData(input, length, allowEndOfInput);\n    }\n\n    @Override\n    public void sampleData(ParsableByteArray data, int length) {\n      sampleQueue.sampleData(data, length);\n    }\n\n    @Override\n    public void sampleMetadata(\n        long timeUs, int flags, int size, int offset, @Nullable CryptoData encryptionData) {\n      sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData);\n      parseAndDiscardSamples();\n    }\n\n    /**\n     * For live streaming, check if the DASH manifest is expired before the next segment start time.\n     * If it is, the DASH media source will be notified to refresh the manifest.\n     *\n     * @param presentationPositionUs The next load position in presentation time.\n     * @return True if manifest refresh has been requested, false otherwise.\n     */\n    public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {\n      return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk(\n          presentationPositionUs);\n    }\n\n    /**\n     * Called when the a new chunk in the current media stream has been loaded.\n     *\n     * @param chunk The chunk whose load has been completed.\n     */\n    public void onChunkLoadCompleted(Chunk chunk) {\n      PlayerEmsgHandler.this.onChunkLoadCompleted(chunk);\n    }\n\n    /**\n     * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages\n     * that signals end-of-stream or Manifest expiry, which results in load error. In this case, we\n     * should notify the Dash media source to refresh its manifest.\n     *\n     * @param chunk The chunk whose load encountered the error.\n     * @return True if manifest refresh has been requested, false otherwise.\n     */\n    public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {\n      return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk);\n    }\n\n    /** Release this track emsg handler. It should not be reused after this call. */\n    public void release() {\n      sampleQueue.reset();\n    }\n\n    // Internal methods.\n\n    private void parseAndDiscardSamples() {\n      while (sampleQueue.isReady(/* loadingFinished= */ false)) {\n        MetadataInputBuffer inputBuffer = dequeueSample();\n        if (inputBuffer == null) {\n          continue;\n        }\n        long eventTimeUs = inputBuffer.timeUs;\n        Metadata metadata = decoder.decode(inputBuffer);\n        EventMessage eventMessage = (EventMessage) metadata.get(0);\n        if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) {\n          parsePlayerEmsgEvent(eventTimeUs, eventMessage);\n        }\n      }\n      sampleQueue.discardToRead();\n    }\n\n    @Nullable\n    private MetadataInputBuffer dequeueSample() {\n      buffer.clear();\n      int result =\n          sampleQueue.read(\n              formatHolder,\n              buffer,\n              /* formatRequired= */ false,\n              /* loadingFinished= */ false,\n              /* decodeOnlyUntilUs= */ 0);\n      if (result == C.RESULT_BUFFER_READ) {\n        buffer.flip();\n        return buffer;\n      }\n      return null;\n    }\n\n    private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) {\n      long manifestPublishTimeMsInEmsg = getManifestPublishTimeMsInEmsg(eventMessage);\n      if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) {\n        return;\n      }\n      onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);\n    }\n\n    private void onManifestExpiredMessageEncountered(\n        long eventTimeUs, long manifestPublishTimeMsInEmsg) {\n      ManifestExpiryEventInfo manifestExpiryEventInfo =\n          new ManifestExpiryEventInfo(eventTimeUs, manifestPublishTimeMsInEmsg);\n      handler.sendMessage(handler.obtainMessage(EMSG_MANIFEST_EXPIRED, manifestExpiryEventInfo));\n    }\n  }\n\n  /** Holds information related to a manifest expiry event. */\n  private static final class ManifestExpiryEventInfo {\n\n    public final long eventTimeUs;\n    public final long manifestPublishTimeMsInEmsg;\n\n    public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) {\n      this.eventTimeUs = eventTimeUs;\n      this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Represents a set of interchangeable encoded versions of a media content component.\n */\npublic class AdaptationSet {\n\n  /**\n   * Value of {@link #id} indicating no value is set.=\n   */\n  public static final int ID_UNSET = -1;\n\n  /**\n   * A non-negative identifier for the adaptation set that's unique in the scope of its containing\n   * period, or {@link #ID_UNSET} if not specified.\n   */\n  public final int id;\n\n  /**\n   * The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}\n   * {@code TRACK_TYPE_*} constants.\n   */\n  public final int type;\n\n  /**\n   * {@link Representation}s in the adaptation set.\n   */\n  public final List<Representation> representations;\n\n  /**\n   * Accessibility descriptors in the adaptation set.\n   */\n  public final List<Descriptor> accessibilityDescriptors;\n\n  /**\n   * Supplemental properties in the adaptation set.\n   */\n  public final List<Descriptor> supplementalProperties;\n\n  /**\n   * @param id A non-negative identifier for the adaptation set that's unique in the scope of its\n   *     containing period, or {@link #ID_UNSET} if not specified.\n   * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}\n   *     {@code TRACK_TYPE_*} constants.\n   * @param representations {@link Representation}s in the adaptation set.\n   * @param accessibilityDescriptors Accessibility descriptors in the adaptation set.\n   * @param supplementalProperties Supplemental properties in the adaptation set.\n   */\n  public AdaptationSet(int id, int type, List<Representation> representations,\n      List<Descriptor> accessibilityDescriptors, List<Descriptor> supplementalProperties) {\n    this.id = id;\n    this.type = type;\n    this.representations = Collections.unmodifiableList(representations);\n    this.accessibilityDescriptors =\n        accessibilityDescriptors == null\n            ? Collections.emptyList()\n            : Collections.unmodifiableList(accessibilityDescriptors);\n    this.supplementalProperties =\n        supplementalProperties == null\n            ? Collections.emptyList()\n            : Collections.unmodifiableList(supplementalProperties);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.FilterableManifest;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014\n * Section 5.3.1.2.\n */\npublic class DashManifest implements FilterableManifest<DashManifest> {\n\n  /**\n   * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if\n   * not present.\n   */\n  public final long availabilityStartTimeMs;\n\n  /**\n   * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable.\n   */\n  public final long durationMs;\n\n  /**\n   * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present.\n   */\n  public final long minBufferTimeMs;\n\n  /**\n   * Whether the manifest has value \"dynamic\" for the {@code type} attribute.\n   */\n  public final boolean dynamic;\n\n  /**\n   * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not\n   * applicable.\n   */\n  public final long minUpdatePeriodMs;\n\n  /**\n   * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not\n   * present.\n   */\n  public final long timeShiftBufferDepthMs;\n\n  /**\n   * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not\n   * present.\n   */\n  public final long suggestedPresentationDelayMs;\n\n  /**\n   * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if\n   * not present.\n   */\n  public final long publishTimeMs;\n\n  /**\n   * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section\n   * 4.7.2.\n   */\n  @Nullable public final UtcTimingElement utcTiming;\n\n  /** The location of this manifest, or null if not present. */\n  @Nullable public final Uri location;\n\n  /** The {@link ProgramInformation}, or null if not present. */\n  @Nullable public final ProgramInformation programInformation;\n\n  private final List<Period> periods;\n\n  /**\n   * @deprecated Use {@link #DashManifest(long, long, long, boolean, long, long, long, long,\n   *     ProgramInformation, UtcTimingElement, Uri, List)}.\n   */\n  @Deprecated\n  public DashManifest(\n      long availabilityStartTimeMs,\n      long durationMs,\n      long minBufferTimeMs,\n      boolean dynamic,\n      long minUpdatePeriodMs,\n      long timeShiftBufferDepthMs,\n      long suggestedPresentationDelayMs,\n      long publishTimeMs,\n      @Nullable UtcTimingElement utcTiming,\n      @Nullable Uri location,\n      List<Period> periods) {\n    this(\n        availabilityStartTimeMs,\n        durationMs,\n        minBufferTimeMs,\n        dynamic,\n        minUpdatePeriodMs,\n        timeShiftBufferDepthMs,\n        suggestedPresentationDelayMs,\n        publishTimeMs,\n        /* programInformation= */ null,\n        utcTiming,\n        location,\n        periods);\n  }\n\n  public DashManifest(\n      long availabilityStartTimeMs,\n      long durationMs,\n      long minBufferTimeMs,\n      boolean dynamic,\n      long minUpdatePeriodMs,\n      long timeShiftBufferDepthMs,\n      long suggestedPresentationDelayMs,\n      long publishTimeMs,\n      @Nullable ProgramInformation programInformation,\n      @Nullable UtcTimingElement utcTiming,\n      @Nullable Uri location,\n      List<Period> periods) {\n    this.availabilityStartTimeMs = availabilityStartTimeMs;\n    this.durationMs = durationMs;\n    this.minBufferTimeMs = minBufferTimeMs;\n    this.dynamic = dynamic;\n    this.minUpdatePeriodMs = minUpdatePeriodMs;\n    this.timeShiftBufferDepthMs = timeShiftBufferDepthMs;\n    this.suggestedPresentationDelayMs = suggestedPresentationDelayMs;\n    this.publishTimeMs = publishTimeMs;\n    this.programInformation = programInformation;\n    this.utcTiming = utcTiming;\n    this.location = location;\n    this.periods = periods == null ? Collections.emptyList() : periods;\n  }\n\n  public final int getPeriodCount() {\n    return periods.size();\n  }\n\n  public final Period getPeriod(int index) {\n    return periods.get(index);\n  }\n\n  public final long getPeriodDurationMs(int index) {\n    return index == periods.size() - 1\n        ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs))\n        : (periods.get(index + 1).startMs - periods.get(index).startMs);\n  }\n\n  public final long getPeriodDurationUs(int index) {\n    return C.msToUs(getPeriodDurationMs(index));\n  }\n\n  @Override\n  public final DashManifest copy(List<StreamKey> streamKeys) {\n    LinkedList<StreamKey> keys = new LinkedList<>(streamKeys);\n    Collections.sort(keys);\n    keys.add(new StreamKey(-1, -1, -1)); // Add a stopper key to the end\n\n    ArrayList<Period> copyPeriods = new ArrayList<>();\n    long shiftMs = 0;\n    for (int periodIndex = 0; periodIndex < getPeriodCount(); periodIndex++) {\n      if (keys.peek().periodIndex != periodIndex) {\n        // No representations selected in this period.\n        long periodDurationMs = getPeriodDurationMs(periodIndex);\n        if (periodDurationMs != C.TIME_UNSET) {\n          shiftMs += periodDurationMs;\n        }\n      } else {\n        Period period = getPeriod(periodIndex);\n        ArrayList<AdaptationSet> copyAdaptationSets =\n            copyAdaptationSets(period.adaptationSets, keys);\n        Period copiedPeriod = new Period(period.id, period.startMs - shiftMs, copyAdaptationSets,\n            period.eventStreams);\n        copyPeriods.add(copiedPeriod);\n      }\n    }\n    long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET;\n    return new DashManifest(\n        availabilityStartTimeMs,\n        newDuration,\n        minBufferTimeMs,\n        dynamic,\n        minUpdatePeriodMs,\n        timeShiftBufferDepthMs,\n        suggestedPresentationDelayMs,\n        publishTimeMs,\n        programInformation,\n        utcTiming,\n        location,\n        copyPeriods);\n  }\n\n  private static ArrayList<AdaptationSet> copyAdaptationSets(\n      List<AdaptationSet> adaptationSets, LinkedList<StreamKey> keys) {\n    StreamKey key = keys.poll();\n    int periodIndex = key.periodIndex;\n    ArrayList<AdaptationSet> copyAdaptationSets = new ArrayList<>();\n    do {\n      int adaptationSetIndex = key.groupIndex;\n      AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex);\n\n      List<Representation> representations = adaptationSet.representations;\n      ArrayList<Representation> copyRepresentations = new ArrayList<>();\n      do {\n        Representation representation = representations.get(key.trackIndex);\n        copyRepresentations.add(representation);\n        key = keys.poll();\n      } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex);\n\n      copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type,\n          copyRepresentations, adaptationSet.accessibilityDescriptors,\n          adaptationSet.supplementalProperties));\n    } while(key.periodIndex == periodIndex);\n    // Add back the last key which doesn't belong to the period being processed\n    keys.addFirst(key);\n    return copyAdaptationSets;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Pair;\nimport android.util.Xml;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.util.XmlPullParserUtil;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.xml.sax.helpers.DefaultHandler;\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\nimport org.xmlpull.v1.XmlPullParserFactory;\nimport org.xmlpull.v1.XmlSerializer;\n\n/**\n * A parser of media presentation description files.\n */\npublic class DashManifestParser extends DefaultHandler\n    implements ParsingLoadable.Parser<DashManifest> {\n\n  private static final String TAG = \"MpdParser\";\n\n  private static final Pattern FRAME_RATE_PATTERN = Pattern.compile(\"(\\\\d+)(?:/(\\\\d+))?\");\n\n  private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile(\"CC([1-4])=.*\");\n  private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =\n      Pattern.compile(\"([1-9]|[1-5][0-9]|6[0-3])=.*\");\n\n  private final XmlPullParserFactory xmlParserFactory;\n\n  public DashManifestParser() {\n    try {\n      xmlParserFactory = XmlPullParserFactory.newInstance();\n    } catch (XmlPullParserException e) {\n      throw new RuntimeException(\"Couldn't create XmlPullParserFactory instance\", e);\n    }\n  }\n\n  // MPD parsing.\n\n  @Override\n  public DashManifest parse(Uri uri, InputStream inputStream) throws IOException {\n    try {\n      XmlPullParser xpp = xmlParserFactory.newPullParser();\n      xpp.setInput(inputStream, null);\n      int eventType = xpp.next();\n      if (eventType != XmlPullParser.START_TAG || !\"MPD\".equals(xpp.getName())) {\n        throw new ParserException(\n            \"inputStream does not contain a valid media presentation description\");\n      }\n      return parseMediaPresentationDescription(xpp, uri.toString());\n    } catch (XmlPullParserException e) {\n      throw new ParserException(e);\n    }\n  }\n\n  protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp,\n      String baseUrl) throws XmlPullParserException, IOException {\n    long availabilityStartTime = parseDateTime(xpp, \"availabilityStartTime\", C.TIME_UNSET);\n    long durationMs = parseDuration(xpp, \"mediaPresentationDuration\", C.TIME_UNSET);\n    long minBufferTimeMs = parseDuration(xpp, \"minBufferTime\", C.TIME_UNSET);\n    String typeString = xpp.getAttributeValue(null, \"type\");\n    boolean dynamic = \"dynamic\".equals(typeString);\n    long minUpdateTimeMs = dynamic ? parseDuration(xpp, \"minimumUpdatePeriod\", C.TIME_UNSET)\n        : C.TIME_UNSET;\n    long timeShiftBufferDepthMs = dynamic\n        ? parseDuration(xpp, \"timeShiftBufferDepth\", C.TIME_UNSET) : C.TIME_UNSET;\n    long suggestedPresentationDelayMs = dynamic\n        ? parseDuration(xpp, \"suggestedPresentationDelay\", C.TIME_UNSET) : C.TIME_UNSET;\n    long publishTimeMs = parseDateTime(xpp, \"publishTime\", C.TIME_UNSET);\n    ProgramInformation programInformation = null;\n    UtcTimingElement utcTiming = null;\n    Uri location = null;\n\n    List<Period> periods = new ArrayList<>();\n    long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0;\n    boolean seenEarlyAccessPeriod = false;\n    boolean seenFirstBaseUrl = false;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"BaseURL\")) {\n        if (!seenFirstBaseUrl) {\n          baseUrl = parseBaseUrl(xpp, baseUrl);\n          seenFirstBaseUrl = true;\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"ProgramInformation\")) {\n        programInformation = parseProgramInformation(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"UTCTiming\")) {\n        utcTiming = parseUtcTiming(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Location\")) {\n        location = Uri.parse(xpp.nextText());\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Period\") && !seenEarlyAccessPeriod) {\n        Pair<Period, Long> periodWithDurationMs = parsePeriod(xpp, baseUrl, nextPeriodStartMs);\n        Period period = periodWithDurationMs.first;\n        if (period.startMs == C.TIME_UNSET) {\n          if (dynamic) {\n            // This is an early access period. Ignore it. All subsequent periods must also be\n            // early access.\n            seenEarlyAccessPeriod = true;\n          } else {\n            throw new ParserException(\"Unable to determine start of period \" + periods.size());\n          }\n        } else {\n          long periodDurationMs = periodWithDurationMs.second;\n          nextPeriodStartMs = periodDurationMs == C.TIME_UNSET ? C.TIME_UNSET\n              : (period.startMs + periodDurationMs);\n          periods.add(period);\n        }\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"MPD\"));\n\n    if (durationMs == C.TIME_UNSET) {\n      if (nextPeriodStartMs != C.TIME_UNSET) {\n        // If we know the end time of the final period, we can use it as the duration.\n        durationMs = nextPeriodStartMs;\n      } else if (!dynamic) {\n        throw new ParserException(\"Unable to determine duration of static manifest.\");\n      }\n    }\n\n    if (periods.isEmpty()) {\n      throw new ParserException(\"No periods found.\");\n    }\n\n    return buildMediaPresentationDescription(\n        availabilityStartTime,\n        durationMs,\n        minBufferTimeMs,\n        dynamic,\n        minUpdateTimeMs,\n        timeShiftBufferDepthMs,\n        suggestedPresentationDelayMs,\n        publishTimeMs,\n        programInformation,\n        utcTiming,\n        location,\n        periods);\n  }\n\n  protected DashManifest buildMediaPresentationDescription(\n      long availabilityStartTime,\n      long durationMs,\n      long minBufferTimeMs,\n      boolean dynamic,\n      long minUpdateTimeMs,\n      long timeShiftBufferDepthMs,\n      long suggestedPresentationDelayMs,\n      long publishTimeMs,\n      @Nullable ProgramInformation programInformation,\n      @Nullable UtcTimingElement utcTiming,\n      @Nullable Uri location,\n      List<Period> periods) {\n    return new DashManifest(\n        availabilityStartTime,\n        durationMs,\n        minBufferTimeMs,\n        dynamic,\n        minUpdateTimeMs,\n        timeShiftBufferDepthMs,\n        suggestedPresentationDelayMs,\n        publishTimeMs,\n        programInformation,\n        utcTiming,\n        location,\n        periods);\n  }\n\n  protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) {\n    String schemeIdUri = xpp.getAttributeValue(null, \"schemeIdUri\");\n    String value = xpp.getAttributeValue(null, \"value\");\n    return buildUtcTimingElement(schemeIdUri, value);\n  }\n\n  protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) {\n    return new UtcTimingElement(schemeIdUri, value);\n  }\n\n  protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs)\n      throws XmlPullParserException, IOException {\n    String id = xpp.getAttributeValue(null, \"id\");\n    long startMs = parseDuration(xpp, \"start\", defaultStartMs);\n    long durationMs = parseDuration(xpp, \"duration\", C.TIME_UNSET);\n    SegmentBase segmentBase = null;\n    List<AdaptationSet> adaptationSets = new ArrayList<>();\n    List<EventStream> eventStreams = new ArrayList<>();\n    boolean seenFirstBaseUrl = false;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"BaseURL\")) {\n        if (!seenFirstBaseUrl) {\n          baseUrl = parseBaseUrl(xpp, baseUrl);\n          seenFirstBaseUrl = true;\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"AdaptationSet\")) {\n        adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase, durationMs));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"EventStream\")) {\n        eventStreams.add(parseEventStream(xpp));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentBase\")) {\n        segmentBase = parseSegmentBase(xpp, null);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentList\")) {\n        segmentBase = parseSegmentList(xpp, null, durationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs);\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"Period\"));\n\n    return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs);\n  }\n\n  protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets,\n      List<EventStream> eventStreams) {\n    return new Period(id, startMs, adaptationSets, eventStreams);\n  }\n\n  // AdaptationSet parsing.\n\n  protected AdaptationSet parseAdaptationSet(\n      XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase, long periodDurationMs)\n      throws XmlPullParserException, IOException {\n    int id = parseInt(xpp, \"id\", AdaptationSet.ID_UNSET);\n    int contentType = parseContentType(xpp);\n\n    String mimeType = xpp.getAttributeValue(null, \"mimeType\");\n    String codecs = xpp.getAttributeValue(null, \"codecs\");\n    int width = parseInt(xpp, \"width\", Format.NO_VALUE);\n    int height = parseInt(xpp, \"height\", Format.NO_VALUE);\n    float frameRate = parseFrameRate(xpp, Format.NO_VALUE);\n    int audioChannels = Format.NO_VALUE;\n    int audioSamplingRate = parseInt(xpp, \"audioSamplingRate\", Format.NO_VALUE);\n    String language = xpp.getAttributeValue(null, \"lang\");\n    String label = xpp.getAttributeValue(null, \"label\");\n    String drmSchemeType = null;\n    ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\n    ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();\n    ArrayList<Descriptor> accessibilityDescriptors = new ArrayList<>();\n    ArrayList<Descriptor> roleDescriptors = new ArrayList<>();\n    ArrayList<Descriptor> supplementalProperties = new ArrayList<>();\n    List<RepresentationInfo> representationInfos = new ArrayList<>();\n\n    boolean seenFirstBaseUrl = false;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"BaseURL\")) {\n        if (!seenFirstBaseUrl) {\n          baseUrl = parseBaseUrl(xpp, baseUrl);\n          seenFirstBaseUrl = true;\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"ContentProtection\")) {\n        Pair<String, SchemeData> contentProtection = parseContentProtection(xpp);\n        if (contentProtection.first != null) {\n          drmSchemeType = contentProtection.first;\n        }\n        if (contentProtection.second != null) {\n          drmSchemeDatas.add(contentProtection.second);\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"ContentComponent\")) {\n        language = checkLanguageConsistency(language, xpp.getAttributeValue(null, \"lang\"));\n        contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Role\")) {\n        roleDescriptors.add(parseDescriptor(xpp, \"Role\"));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"AudioChannelConfiguration\")) {\n        audioChannels = parseAudioChannelConfiguration(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Accessibility\")) {\n        accessibilityDescriptors.add(parseDescriptor(xpp, \"Accessibility\"));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SupplementalProperty\")) {\n        supplementalProperties.add(parseDescriptor(xpp, \"SupplementalProperty\"));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Representation\")) {\n        RepresentationInfo representationInfo =\n            parseRepresentation(\n                xpp,\n                baseUrl,\n                mimeType,\n                codecs,\n                width,\n                height,\n                frameRate,\n                audioChannels,\n                audioSamplingRate,\n                language,\n                roleDescriptors,\n                accessibilityDescriptors,\n                supplementalProperties,\n                segmentBase,\n                periodDurationMs);\n        contentType = checkContentTypeConsistency(contentType,\n            getContentType(representationInfo.format));\n        representationInfos.add(representationInfo);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentBase\")) {\n        segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentList\")) {\n        segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase =\n            parseSegmentTemplate(\n                xpp, (SegmentTemplate) segmentBase, supplementalProperties, periodDurationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"InbandEventStream\")) {\n        inbandEventStreams.add(parseDescriptor(xpp, \"InbandEventStream\"));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Label\")) {\n        label = parseLabel(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp)) {\n        parseAdaptationSetChild(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"AdaptationSet\"));\n\n    // Build the representations.\n    List<Representation> representations = new ArrayList<>(representationInfos.size());\n    for (int i = 0; i < representationInfos.size(); i++) {\n      representations.add(\n          buildRepresentation(\n              representationInfos.get(i),\n              label,\n              drmSchemeType,\n              drmSchemeDatas,\n              inbandEventStreams));\n    }\n\n    return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors,\n        supplementalProperties);\n  }\n\n  protected AdaptationSet buildAdaptationSet(int id, int contentType,\n      List<Representation> representations, List<Descriptor> accessibilityDescriptors,\n      List<Descriptor> supplementalProperties) {\n    return new AdaptationSet(id, contentType, representations, accessibilityDescriptors,\n        supplementalProperties);\n  }\n\n  protected int parseContentType(XmlPullParser xpp) {\n    String contentType = xpp.getAttributeValue(null, \"contentType\");\n    return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN\n        : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO\n            : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO\n                : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT\n                    : C.TRACK_TYPE_UNKNOWN;\n  }\n\n  protected int getContentType(Format format) {\n    String sampleMimeType = format.sampleMimeType;\n    if (TextUtils.isEmpty(sampleMimeType)) {\n      return C.TRACK_TYPE_UNKNOWN;\n    } else if (MimeTypes.isVideo(sampleMimeType)) {\n      return C.TRACK_TYPE_VIDEO;\n    } else if (MimeTypes.isAudio(sampleMimeType)) {\n      return C.TRACK_TYPE_AUDIO;\n    } else if (mimeTypeIsRawText(sampleMimeType)) {\n      return C.TRACK_TYPE_TEXT;\n    }\n    return C.TRACK_TYPE_UNKNOWN;\n  }\n\n  /**\n   * Parses a ContentProtection element.\n   *\n   * @param xpp The parser from which to read.\n   * @throws XmlPullParserException If an error occurs parsing the element.\n   * @throws IOException If an error occurs reading the element.\n   * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element.\n   *     Either or both may be null, depending on the ContentProtection element being parsed.\n   */\n  protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection(\n      XmlPullParser xpp) throws XmlPullParserException, IOException {\n    String schemeType = null;\n    String licenseServerUrl = null;\n    byte[] data = null;\n    UUID uuid = null;\n\n    String schemeIdUri = xpp.getAttributeValue(null, \"schemeIdUri\");\n    if (schemeIdUri != null) {\n      switch (Util.toLowerInvariant(schemeIdUri)) {\n        case \"urn:mpeg:dash:mp4protection:2011\":\n          schemeType = xpp.getAttributeValue(null, \"value\");\n          String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, \"default_KID\");\n          if (!TextUtils.isEmpty(defaultKid)\n              && !\"00000000-0000-0000-0000-000000000000\".equals(defaultKid)) {\n            String[] defaultKidStrings = defaultKid.split(\"\\\\s+\");\n            UUID[] defaultKids = new UUID[defaultKidStrings.length];\n            for (int i = 0; i < defaultKidStrings.length; i++) {\n              defaultKids[i] = UUID.fromString(defaultKidStrings[i]);\n            }\n            data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null);\n            uuid = C.COMMON_PSSH_UUID;\n          }\n          break;\n        case \"urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95\":\n          uuid = C.PLAYREADY_UUID;\n          break;\n        case \"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\":\n          uuid = C.WIDEVINE_UUID;\n          break;\n        default:\n          break;\n      }\n    }\n\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"ms:laurl\")) {\n        licenseServerUrl = xpp.getAttributeValue(null, \"licenseUrl\");\n      } else if (data == null\n          && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, \"pssh\")\n          && xpp.next() == XmlPullParser.TEXT) {\n        // The cenc:pssh element is defined in 23001-7:2015.\n        data = Base64.decode(xpp.getText(), Base64.DEFAULT);\n        uuid = PsshAtomUtil.parseUuid(data);\n        if (uuid == null) {\n          Log.w(TAG, \"Skipping malformed cenc:pssh data\");\n          data = null;\n        }\n      } else if (data == null\n          && C.PLAYREADY_UUID.equals(uuid)\n          && XmlPullParserUtil.isStartTag(xpp, \"mspr:pro\")\n          && xpp.next() == XmlPullParser.TEXT) {\n        // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady.\n        data =\n            PsshAtomUtil.buildPsshAtom(\n                C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT));\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"ContentProtection\"));\n    SchemeData schemeData =\n        uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null;\n    return Pair.create(schemeType, schemeData);\n  }\n\n  /**\n   * Parses children of AdaptationSet elements not specifically parsed elsewhere.\n   *\n   * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed.\n   * @throws XmlPullParserException If an error occurs parsing the element.\n   * @throws IOException If an error occurs reading the element.\n   */\n  protected void parseAdaptationSetChild(XmlPullParser xpp)\n      throws XmlPullParserException, IOException {\n    maybeSkipTag(xpp);\n  }\n\n  // Representation parsing.\n\n  protected RepresentationInfo parseRepresentation(\n      XmlPullParser xpp,\n      String baseUrl,\n      @Nullable String adaptationSetMimeType,\n      @Nullable String adaptationSetCodecs,\n      int adaptationSetWidth,\n      int adaptationSetHeight,\n      float adaptationSetFrameRate,\n      int adaptationSetAudioChannels,\n      int adaptationSetAudioSamplingRate,\n      @Nullable String adaptationSetLanguage,\n      List<Descriptor> adaptationSetRoleDescriptors,\n      List<Descriptor> adaptationSetAccessibilityDescriptors,\n      List<Descriptor> adaptationSetSupplementalProperties,\n      @Nullable SegmentBase segmentBase,\n      long periodDurationMs)\n      throws XmlPullParserException, IOException {\n    String id = xpp.getAttributeValue(null, \"id\");\n    int bandwidth = parseInt(xpp, \"bandwidth\", Format.NO_VALUE);\n\n    String mimeType = parseString(xpp, \"mimeType\", adaptationSetMimeType);\n    String codecs = parseString(xpp, \"codecs\", adaptationSetCodecs);\n    int width = parseInt(xpp, \"width\", adaptationSetWidth);\n    int height = parseInt(xpp, \"height\", adaptationSetHeight);\n    float frameRate = parseFrameRate(xpp, adaptationSetFrameRate);\n    int audioChannels = adaptationSetAudioChannels;\n    int audioSamplingRate = parseInt(xpp, \"audioSamplingRate\", adaptationSetAudioSamplingRate);\n    String drmSchemeType = null;\n    ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\n    ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();\n    ArrayList<Descriptor> supplementalProperties = new ArrayList<>();\n\n    boolean seenFirstBaseUrl = false;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"BaseURL\")) {\n        if (!seenFirstBaseUrl) {\n          baseUrl = parseBaseUrl(xpp, baseUrl);\n          seenFirstBaseUrl = true;\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"AudioChannelConfiguration\")) {\n        audioChannels = parseAudioChannelConfiguration(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentBase\")) {\n        segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentList\")) {\n        segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase =\n            parseSegmentTemplate(\n                xpp,\n                (SegmentTemplate) segmentBase,\n                adaptationSetSupplementalProperties,\n                periodDurationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"ContentProtection\")) {\n        Pair<String, SchemeData> contentProtection = parseContentProtection(xpp);\n        if (contentProtection.first != null) {\n          drmSchemeType = contentProtection.first;\n        }\n        if (contentProtection.second != null) {\n          drmSchemeDatas.add(contentProtection.second);\n        }\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"InbandEventStream\")) {\n        inbandEventStreams.add(parseDescriptor(xpp, \"InbandEventStream\"));\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SupplementalProperty\")) {\n        supplementalProperties.add(parseDescriptor(xpp, \"SupplementalProperty\"));\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"Representation\"));\n\n    Format format =\n        buildFormat(\n            id,\n            mimeType,\n            width,\n            height,\n            frameRate,\n            audioChannels,\n            audioSamplingRate,\n            bandwidth,\n            adaptationSetLanguage,\n            adaptationSetRoleDescriptors,\n            adaptationSetAccessibilityDescriptors,\n            codecs,\n            supplementalProperties);\n    segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();\n\n    return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas,\n        inbandEventStreams, Representation.REVISION_ID_DEFAULT);\n  }\n\n  protected Format buildFormat(\n      @Nullable String id,\n      @Nullable String containerMimeType,\n      int width,\n      int height,\n      float frameRate,\n      int audioChannels,\n      int audioSamplingRate,\n      int bitrate,\n      @Nullable String language,\n      List<Descriptor> roleDescriptors,\n      List<Descriptor> accessibilityDescriptors,\n      @Nullable String codecs,\n      List<Descriptor> supplementalProperties) {\n    String sampleMimeType = getSampleMimeType(containerMimeType, codecs);\n    @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors);\n    @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors);\n    roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors);\n    if (sampleMimeType != null) {\n      if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) {\n        sampleMimeType = parseEac3SupplementalProperties(supplementalProperties);\n      }\n      if (MimeTypes.isVideo(sampleMimeType)) {\n        return Format.createVideoContainerFormat(\n            id,\n            /* label= */ null,\n            containerMimeType,\n            sampleMimeType,\n            codecs,\n            /* metadata= */ null,\n            bitrate,\n            width,\n            height,\n            frameRate,\n            /* initializationData= */ null,\n            selectionFlags,\n            roleFlags);\n      } else if (MimeTypes.isAudio(sampleMimeType)) {\n        return Format.createAudioContainerFormat(\n            id,\n            /* label= */ null,\n            containerMimeType,\n            sampleMimeType,\n            codecs,\n            /* metadata= */ null,\n            bitrate,\n            audioChannels,\n            audioSamplingRate,\n            /* initializationData= */ null,\n            selectionFlags,\n            roleFlags,\n            language);\n      } else if (mimeTypeIsRawText(sampleMimeType)) {\n        int accessibilityChannel;\n        if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) {\n          accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors);\n        } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) {\n          accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors);\n        } else {\n          accessibilityChannel = Format.NO_VALUE;\n        }\n        return Format.createTextContainerFormat(\n            id,\n            /* label= */ null,\n            containerMimeType,\n            sampleMimeType,\n            codecs,\n            bitrate,\n            selectionFlags,\n            roleFlags,\n            language,\n            accessibilityChannel);\n      }\n    }\n    return Format.createContainerFormat(\n        id,\n        /* label= */ null,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        bitrate,\n        selectionFlags,\n        roleFlags,\n        language);\n  }\n\n  protected Representation buildRepresentation(\n      RepresentationInfo representationInfo,\n      @Nullable String label,\n      @Nullable String extraDrmSchemeType,\n      ArrayList<SchemeData> extraDrmSchemeDatas,\n      ArrayList<Descriptor> extraInbandEventStreams) {\n    Format format = representationInfo.format;\n    if (label != null) {\n      format = format.copyWithLabel(label);\n    }\n    String drmSchemeType = representationInfo.drmSchemeType != null\n        ? representationInfo.drmSchemeType : extraDrmSchemeType;\n    ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;\n    drmSchemeDatas.addAll(extraDrmSchemeDatas);\n    if (!drmSchemeDatas.isEmpty()) {\n      filterRedundantIncompleteSchemeDatas(drmSchemeDatas);\n      DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas);\n      format = format.copyWithDrmInitData(drmInitData);\n    }\n    ArrayList<Descriptor> inbandEventStreams = representationInfo.inbandEventStreams;\n    inbandEventStreams.addAll(extraInbandEventStreams);\n    return Representation.newInstance(\n        representationInfo.revisionId,\n        format,\n        representationInfo.baseUrl,\n        representationInfo.segmentBase,\n        inbandEventStreams);\n  }\n\n  // SegmentBase, SegmentList and SegmentTemplate parsing.\n\n  protected SingleSegmentBase parseSegmentBase(\n      XmlPullParser xpp, @Nullable SingleSegmentBase parent)\n      throws XmlPullParserException, IOException {\n\n    long timescale = parseLong(xpp, \"timescale\", parent != null ? parent.timescale : 1);\n    long presentationTimeOffset = parseLong(xpp, \"presentationTimeOffset\",\n        parent != null ? parent.presentationTimeOffset : 0);\n\n    long indexStart = parent != null ? parent.indexStart : 0;\n    long indexLength = parent != null ? parent.indexLength : 0;\n    String indexRangeText = xpp.getAttributeValue(null, \"indexRange\");\n    if (indexRangeText != null) {\n      String[] indexRange = indexRangeText.split(\"-\");\n      indexStart = Long.parseLong(indexRange[0]);\n      indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;\n    }\n\n    RangedUri initialization = parent != null ? parent.initialization : null;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"Initialization\")) {\n        initialization = parseInitialization(xpp);\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"SegmentBase\"));\n\n    return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,\n        indexLength);\n  }\n\n  protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale,\n      long presentationTimeOffset, long indexStart, long indexLength) {\n    return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,\n        indexLength);\n  }\n\n  protected SegmentList parseSegmentList(\n      XmlPullParser xpp, @Nullable SegmentList parent, long periodDurationMs)\n      throws XmlPullParserException, IOException {\n\n    long timescale = parseLong(xpp, \"timescale\", parent != null ? parent.timescale : 1);\n    long presentationTimeOffset = parseLong(xpp, \"presentationTimeOffset\",\n        parent != null ? parent.presentationTimeOffset : 0);\n    long duration = parseLong(xpp, \"duration\", parent != null ? parent.duration : C.TIME_UNSET);\n    long startNumber = parseLong(xpp, \"startNumber\", parent != null ? parent.startNumber : 1);\n\n    RangedUri initialization = null;\n    List<SegmentTimelineElement> timeline = null;\n    List<RangedUri> segments = null;\n\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"Initialization\")) {\n        initialization = parseInitialization(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTimeline\")) {\n        timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentURL\")) {\n        if (segments == null) {\n          segments = new ArrayList<>();\n        }\n        segments.add(parseSegmentUrl(xpp));\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"SegmentList\"));\n\n    if (parent != null) {\n      initialization = initialization != null ? initialization : parent.initialization;\n      timeline = timeline != null ? timeline : parent.segmentTimeline;\n      segments = segments != null ? segments : parent.mediaSegments;\n    }\n\n    return buildSegmentList(initialization, timescale, presentationTimeOffset,\n        startNumber, duration, timeline, segments);\n  }\n\n  protected SegmentList buildSegmentList(\n      RangedUri initialization,\n      long timescale,\n      long presentationTimeOffset,\n      long startNumber,\n      long duration,\n      @Nullable List<SegmentTimelineElement> timeline,\n      @Nullable List<RangedUri> segments) {\n    return new SegmentList(initialization, timescale, presentationTimeOffset,\n        startNumber, duration, timeline, segments);\n  }\n\n  protected SegmentTemplate parseSegmentTemplate(\n      XmlPullParser xpp,\n      @Nullable SegmentTemplate parent,\n      List<Descriptor> adaptationSetSupplementalProperties,\n      long periodDurationMs)\n      throws XmlPullParserException, IOException {\n    long timescale = parseLong(xpp, \"timescale\", parent != null ? parent.timescale : 1);\n    long presentationTimeOffset = parseLong(xpp, \"presentationTimeOffset\",\n        parent != null ? parent.presentationTimeOffset : 0);\n    long duration = parseLong(xpp, \"duration\", parent != null ? parent.duration : C.TIME_UNSET);\n    long startNumber = parseLong(xpp, \"startNumber\", parent != null ? parent.startNumber : 1);\n    long endNumber =\n        parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties);\n\n    UrlTemplate mediaTemplate = parseUrlTemplate(xpp, \"media\",\n        parent != null ? parent.mediaTemplate : null);\n    UrlTemplate initializationTemplate = parseUrlTemplate(xpp, \"initialization\",\n        parent != null ? parent.initializationTemplate : null);\n\n    RangedUri initialization = null;\n    List<SegmentTimelineElement> timeline = null;\n\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"Initialization\")) {\n        initialization = parseInitialization(xpp);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTimeline\")) {\n        timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs);\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"SegmentTemplate\"));\n\n    if (parent != null) {\n      initialization = initialization != null ? initialization : parent.initialization;\n      timeline = timeline != null ? timeline : parent.segmentTimeline;\n    }\n\n    return buildSegmentTemplate(\n        initialization,\n        timescale,\n        presentationTimeOffset,\n        startNumber,\n        endNumber,\n        duration,\n        timeline,\n        initializationTemplate,\n        mediaTemplate);\n  }\n\n  protected SegmentTemplate buildSegmentTemplate(\n      RangedUri initialization,\n      long timescale,\n      long presentationTimeOffset,\n      long startNumber,\n      long endNumber,\n      long duration,\n      List<SegmentTimelineElement> timeline,\n      @Nullable UrlTemplate initializationTemplate,\n      @Nullable UrlTemplate mediaTemplate) {\n    return new SegmentTemplate(\n        initialization,\n        timescale,\n        presentationTimeOffset,\n        startNumber,\n        endNumber,\n        duration,\n        timeline,\n        initializationTemplate,\n        mediaTemplate);\n  }\n\n  /**\n   * /**\n   * Parses a single EventStream node in the manifest.\n   * <p>\n   * @param xpp The current xml parser.\n   * @return The {@link EventStream} parsed from this EventStream node.\n   * @throws XmlPullParserException If there is any error parsing this node.\n   * @throws IOException If there is any error reading from the underlying input stream.\n   */\n  protected EventStream parseEventStream(XmlPullParser xpp)\n      throws XmlPullParserException, IOException {\n    String schemeIdUri = parseString(xpp, \"schemeIdUri\", \"\");\n    String value = parseString(xpp, \"value\", \"\");\n    long timescale = parseLong(xpp, \"timescale\", 1);\n    List<Pair<Long, EventMessage>> eventMessages = new ArrayList<>();\n    ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512);\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"Event\")) {\n        Pair<Long, EventMessage> event =\n            parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream);\n        eventMessages.add(event);\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"EventStream\"));\n\n    long[] presentationTimesUs = new long[eventMessages.size()];\n    EventMessage[] events = new EventMessage[eventMessages.size()];\n    for (int i = 0; i < eventMessages.size(); i++) {\n      Pair<Long, EventMessage> event = eventMessages.get(i);\n      presentationTimesUs[i] = event.first;\n      events[i] = event.second;\n    }\n    return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events);\n  }\n\n  protected EventStream buildEventStream(String schemeIdUri, String value, long timescale,\n      long[] presentationTimesUs, EventMessage[] events) {\n    return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events);\n  }\n\n  /**\n   * Parses a single Event node in the manifest.\n   *\n   * @param xpp The current xml parser.\n   * @param schemeIdUri The schemeIdUri of the parent EventStream.\n   * @param value The schemeIdUri of the parent EventStream.\n   * @param timescale The timescale of the parent EventStream.\n   * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event\n   *     objects.\n   * @return A pair containing the node's presentation timestamp in microseconds and the parsed\n   *     {@link EventMessage}.\n   * @throws XmlPullParserException If there is any error parsing this node.\n   * @throws IOException If there is any error reading from the underlying input stream.\n   */\n  protected Pair<Long, EventMessage> parseEvent(\n      XmlPullParser xpp,\n      String schemeIdUri,\n      String value,\n      long timescale,\n      ByteArrayOutputStream scratchOutputStream)\n      throws IOException, XmlPullParserException {\n    long id = parseLong(xpp, \"id\", 0);\n    long duration = parseLong(xpp, \"duration\", C.TIME_UNSET);\n    long presentationTime = parseLong(xpp, \"presentationTime\", 0);\n    long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale);\n    long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND,\n        timescale);\n    String messageData = parseString(xpp, \"messageData\", null);\n    byte[] eventObject = parseEventObject(xpp, scratchOutputStream);\n    return Pair.create(\n        presentationTimesUs,\n        buildEvent(\n            schemeIdUri,\n            value,\n            id,\n            durationMs,\n            messageData == null ? eventObject : Util.getUtf8Bytes(messageData)));\n  }\n\n  /**\n   * Parses an event object.\n   *\n   * @param xpp The current xml parser.\n   * @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing the object.\n   * @return The serialized byte array.\n   * @throws XmlPullParserException If there is any error parsing this node.\n   * @throws IOException If there is any error reading from the underlying input stream.\n   */\n  protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream)\n      throws XmlPullParserException, IOException {\n    scratchOutputStream.reset();\n    XmlSerializer xmlSerializer = Xml.newSerializer();\n    xmlSerializer.setOutput(scratchOutputStream, C.UTF8_NAME);\n    // Start reading everything between <Event> and </Event>, and serialize them into an Xml\n    // byte array.\n    xpp.nextToken();\n    while (!XmlPullParserUtil.isEndTag(xpp, \"Event\")) {\n      switch (xpp.getEventType()) {\n        case (XmlPullParser.START_DOCUMENT):\n          xmlSerializer.startDocument(null, false);\n          break;\n        case (XmlPullParser.END_DOCUMENT):\n          xmlSerializer.endDocument();\n          break;\n        case (XmlPullParser.START_TAG):\n          xmlSerializer.startTag(xpp.getNamespace(), xpp.getName());\n          for (int i = 0; i < xpp.getAttributeCount(); i++) {\n            xmlSerializer.attribute(xpp.getAttributeNamespace(i), xpp.getAttributeName(i),\n                xpp.getAttributeValue(i));\n          }\n          break;\n        case (XmlPullParser.END_TAG):\n          xmlSerializer.endTag(xpp.getNamespace(), xpp.getName());\n          break;\n        case (XmlPullParser.TEXT):\n          xmlSerializer.text(xpp.getText());\n          break;\n        case (XmlPullParser.CDSECT):\n          xmlSerializer.cdsect(xpp.getText());\n          break;\n        case (XmlPullParser.ENTITY_REF):\n          xmlSerializer.entityRef(xpp.getText());\n          break;\n        case (XmlPullParser.IGNORABLE_WHITESPACE):\n          xmlSerializer.ignorableWhitespace(xpp.getText());\n          break;\n        case (XmlPullParser.PROCESSING_INSTRUCTION):\n          xmlSerializer.processingInstruction(xpp.getText());\n          break;\n        case (XmlPullParser.COMMENT):\n          xmlSerializer.comment(xpp.getText());\n          break;\n        case (XmlPullParser.DOCDECL):\n          xmlSerializer.docdecl(xpp.getText());\n          break;\n        default: // fall out\n      }\n      xpp.nextToken();\n    }\n    xmlSerializer.flush();\n    return scratchOutputStream.toByteArray();\n  }\n\n  protected EventMessage buildEvent(\n      String schemeIdUri, String value, long id, long durationMs, byte[] messageData) {\n    return new EventMessage(schemeIdUri, value, durationMs, id, messageData);\n  }\n\n  protected List<SegmentTimelineElement> parseSegmentTimeline(\n      XmlPullParser xpp, long timescale, long periodDurationMs)\n      throws XmlPullParserException, IOException {\n    List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();\n    long startTime = 0;\n    long elementDuration = C.TIME_UNSET;\n    int elementRepeatCount = 0;\n    boolean havePreviousTimelineElement = false;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"S\")) {\n        long newStartTime = parseLong(xpp, \"t\", C.TIME_UNSET);\n        if (havePreviousTimelineElement) {\n          startTime =\n              addSegmentTimelineElementsToList(\n                  segmentTimeline,\n                  startTime,\n                  elementDuration,\n                  elementRepeatCount,\n                  /* endTime= */ newStartTime);\n        }\n        if (newStartTime != C.TIME_UNSET) {\n          startTime = newStartTime;\n        }\n        elementDuration = parseLong(xpp, \"d\", C.TIME_UNSET);\n        elementRepeatCount = parseInt(xpp, \"r\", 0);\n        havePreviousTimelineElement = true;\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"SegmentTimeline\"));\n    if (havePreviousTimelineElement) {\n      long periodDuration = Util.scaleLargeTimestamp(periodDurationMs, timescale, 1000);\n      addSegmentTimelineElementsToList(\n          segmentTimeline,\n          startTime,\n          elementDuration,\n          elementRepeatCount,\n          /* endTime= */ periodDuration);\n    }\n    return segmentTimeline;\n  }\n\n  /**\n   * Adds timeline elements for one S tag to the segment timeline.\n   *\n   * @param startTime Start time of the first timeline element.\n   * @param elementDuration Duration of one timeline element.\n   * @param elementRepeatCount Number of timeline elements minus one. May be negative to indicate\n   *     that the count is determined by the total duration and the element duration.\n   * @param endTime End time of the last timeline element for this S tag, or {@link C#TIME_UNSET} if\n   *     unknown. Only needed if {@code repeatCount} is negative.\n   * @return Calculated next start time.\n   */\n  private long addSegmentTimelineElementsToList(\n      List<SegmentTimelineElement> segmentTimeline,\n      long startTime,\n      long elementDuration,\n      int elementRepeatCount,\n      long endTime) {\n    int count =\n        elementRepeatCount >= 0\n            ? 1 + elementRepeatCount\n            : (int) Util.ceilDivide(endTime - startTime, elementDuration);\n    for (int i = 0; i < count; i++) {\n      segmentTimeline.add(buildSegmentTimelineElement(startTime, elementDuration));\n      startTime += elementDuration;\n    }\n    return startTime;\n  }\n\n  protected SegmentTimelineElement buildSegmentTimelineElement(long startTime, long duration) {\n    return new SegmentTimelineElement(startTime, duration);\n  }\n\n  @Nullable\n  protected UrlTemplate parseUrlTemplate(\n      XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) {\n    String valueString = xpp.getAttributeValue(null, name);\n    if (valueString != null) {\n      return UrlTemplate.compile(valueString);\n    }\n    return defaultValue;\n  }\n\n  protected RangedUri parseInitialization(XmlPullParser xpp) {\n    return parseRangedUrl(xpp, \"sourceURL\", \"range\");\n  }\n\n  protected RangedUri parseSegmentUrl(XmlPullParser xpp) {\n    return parseRangedUrl(xpp, \"media\", \"mediaRange\");\n  }\n\n  protected RangedUri parseRangedUrl(XmlPullParser xpp, String urlAttribute,\n      String rangeAttribute) {\n    String urlText = xpp.getAttributeValue(null, urlAttribute);\n    long rangeStart = 0;\n    long rangeLength = C.LENGTH_UNSET;\n    String rangeText = xpp.getAttributeValue(null, rangeAttribute);\n    if (rangeText != null) {\n      String[] rangeTextArray = rangeText.split(\"-\");\n      rangeStart = Long.parseLong(rangeTextArray[0]);\n      if (rangeTextArray.length == 2) {\n        rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;\n      }\n    }\n    return buildRangedUri(urlText, rangeStart, rangeLength);\n  }\n\n  protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) {\n    return new RangedUri(urlText, rangeStart, rangeLength);\n  }\n\n  protected ProgramInformation parseProgramInformation(XmlPullParser xpp)\n      throws IOException, XmlPullParserException {\n    String title = null;\n    String source = null;\n    String copyright = null;\n    String moreInformationURL = parseString(xpp, \"moreInformationURL\", null);\n    String lang = parseString(xpp, \"lang\", null);\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"Title\")) {\n        title = xpp.nextText();\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Source\")) {\n        source = xpp.nextText();\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"Copyright\")) {\n        copyright = xpp.nextText();\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"ProgramInformation\"));\n    return new ProgramInformation(title, source, copyright, moreInformationURL, lang);\n  }\n\n  /**\n   * Parses a Label element.\n   *\n   * @param xpp The parser from which to read.\n   * @throws XmlPullParserException If an error occurs parsing the element.\n   * @throws IOException If an error occurs reading the element.\n   * @return The parsed label.\n   */\n  protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException {\n    return parseText(xpp, \"Label\");\n  }\n\n  /**\n   * Parses a BaseURL element.\n   *\n   * @param xpp The parser from which to read.\n   * @param parentBaseUrl A base URL for resolving the parsed URL.\n   * @throws XmlPullParserException If an error occurs parsing the element.\n   * @throws IOException If an error occurs reading the element.\n   * @return The parsed and resolved URL.\n   */\n  protected String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl)\n      throws XmlPullParserException, IOException {\n    return UriUtil.resolve(parentBaseUrl, parseText(xpp, \"BaseURL\"));\n  }\n\n  // AudioChannelConfiguration parsing.\n\n  protected int parseAudioChannelConfiguration(XmlPullParser xpp)\n      throws XmlPullParserException, IOException {\n    String schemeIdUri = parseString(xpp, \"schemeIdUri\", null);\n    int audioChannels =\n        \"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\".equals(schemeIdUri)\n            ? parseInt(xpp, \"value\", Format.NO_VALUE)\n            : (\"tag:dolby.com,2014:dash:audio_channel_configuration:2011\".equals(schemeIdUri)\n                    || \"urn:dolby:dash:audio_channel_configuration:2011\".equals(schemeIdUri)\n                ? parseDolbyChannelConfiguration(xpp)\n                : Format.NO_VALUE);\n    do {\n      xpp.next();\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"AudioChannelConfiguration\"));\n    return audioChannels;\n  }\n\n  // Selection flag parsing.\n\n  protected int parseSelectionFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) {\n    for (int i = 0; i < roleDescriptors.size(); i++) {\n      Descriptor descriptor = roleDescriptors.get(i);\n      if (\"urn:mpeg:dash:role:2011\".equalsIgnoreCase(descriptor.schemeIdUri)\n          && \"main\".equals(descriptor.value)) {\n        return C.SELECTION_FLAG_DEFAULT;\n      }\n    }\n    return 0;\n  }\n\n  // Role and Accessibility parsing.\n\n  @C.RoleFlags\n  protected int parseRoleFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) {\n    @C.RoleFlags int result = 0;\n    for (int i = 0; i < roleDescriptors.size(); i++) {\n      Descriptor descriptor = roleDescriptors.get(i);\n      if (\"urn:mpeg:dash:role:2011\".equalsIgnoreCase(descriptor.schemeIdUri)) {\n        result |= parseDashRoleSchemeValue(descriptor.value);\n      }\n    }\n    return result;\n  }\n\n  @C.RoleFlags\n  protected int parseRoleFlagsFromAccessibilityDescriptors(\n      List<Descriptor> accessibilityDescriptors) {\n    @C.RoleFlags int result = 0;\n    for (int i = 0; i < accessibilityDescriptors.size(); i++) {\n      Descriptor descriptor = accessibilityDescriptors.get(i);\n      if (\"urn:mpeg:dash:role:2011\".equalsIgnoreCase(descriptor.schemeIdUri)) {\n        result |= parseDashRoleSchemeValue(descriptor.value);\n      } else if (\"urn:tva:metadata:cs:AudioPurposeCS:2007\"\n          .equalsIgnoreCase(descriptor.schemeIdUri)) {\n        result |= parseTvaAudioPurposeCsValue(descriptor.value);\n      }\n    }\n    return result;\n  }\n\n  @C.RoleFlags\n  protected int parseDashRoleSchemeValue(@Nullable String value) {\n    if (value == null) {\n      return 0;\n    }\n    switch (value) {\n      case \"main\":\n        return C.ROLE_FLAG_MAIN;\n      case \"alternate\":\n        return C.ROLE_FLAG_ALTERNATE;\n      case \"supplementary\":\n        return C.ROLE_FLAG_SUPPLEMENTARY;\n      case \"commentary\":\n        return C.ROLE_FLAG_COMMENTARY;\n      case \"dub\":\n        return C.ROLE_FLAG_DUB;\n      case \"emergency\":\n        return C.ROLE_FLAG_EMERGENCY;\n      case \"caption\":\n        return C.ROLE_FLAG_CAPTION;\n      case \"subtitle\":\n        return C.ROLE_FLAG_SUBTITLE;\n      case \"sign\":\n        return C.ROLE_FLAG_SIGN;\n      case \"description\":\n        return C.ROLE_FLAG_DESCRIBES_VIDEO;\n      case \"enhanced-audio-intelligibility\":\n        return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY;\n      default:\n        return 0;\n    }\n  }\n\n  @C.RoleFlags\n  protected int parseTvaAudioPurposeCsValue(@Nullable String value) {\n    if (value == null) {\n      return 0;\n    }\n    switch (value) {\n      case \"1\": // Audio description for the visually impaired.\n        return C.ROLE_FLAG_DESCRIBES_VIDEO;\n      case \"2\": // Audio description for the hard of hearing.\n        return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY;\n      case \"3\": // Supplemental commentary.\n        return C.ROLE_FLAG_SUPPLEMENTARY;\n      case \"4\": // Director's commentary.\n        return C.ROLE_FLAG_COMMENTARY;\n      case \"6\": // Main programme audio.\n        return C.ROLE_FLAG_MAIN;\n      default:\n        return 0;\n    }\n  }\n\n  // Utility methods.\n\n  /**\n   * If the provided {@link XmlPullParser} is currently positioned at the start of a tag, skips\n   * forward to the end of that tag.\n   *\n   * @param xpp The {@link XmlPullParser}.\n   * @throws XmlPullParserException If an error occurs parsing the stream.\n   * @throws IOException If an error occurs reading the stream.\n   */\n  public static void maybeSkipTag(XmlPullParser xpp) throws IOException, XmlPullParserException {\n    if (!XmlPullParserUtil.isStartTag(xpp)) {\n      return;\n    }\n    int depth = 1;\n    while (depth != 0) {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp)) {\n        depth++;\n      } else if (XmlPullParserUtil.isEndTag(xpp)) {\n        depth--;\n      }\n    }\n  }\n\n  /**\n   * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}.\n   */\n  private static void filterRedundantIncompleteSchemeDatas(ArrayList<SchemeData> schemeDatas) {\n    for (int i = schemeDatas.size() - 1; i >= 0; i--) {\n      SchemeData schemeData = schemeDatas.get(i);\n      if (!schemeData.hasData()) {\n        for (int j = 0; j < schemeDatas.size(); j++) {\n          if (schemeDatas.get(j).canReplace(schemeData)) {\n            // schemeData is incomplete, but there is another matching SchemeData which does contain\n            // data, so we remove the incomplete one.\n            schemeDatas.remove(i);\n            break;\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Derives a sample mimeType from a container mimeType and codecs attribute.\n   *\n   * @param containerMimeType The mimeType of the container.\n   * @param codecs The codecs attribute.\n   * @return The derived sample mimeType, or null if it could not be derived.\n   */\n  @Nullable\n  private static String getSampleMimeType(\n      @Nullable String containerMimeType, @Nullable String codecs) {\n    if (MimeTypes.isAudio(containerMimeType)) {\n      return MimeTypes.getAudioMediaMimeType(codecs);\n    } else if (MimeTypes.isVideo(containerMimeType)) {\n      return MimeTypes.getVideoMediaMimeType(codecs);\n    } else if (mimeTypeIsRawText(containerMimeType)) {\n      return containerMimeType;\n    } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {\n      if (codecs != null) {\n        if (codecs.startsWith(\"stpp\")) {\n          return MimeTypes.APPLICATION_TTML;\n        } else if (codecs.startsWith(\"wvtt\")) {\n          return MimeTypes.APPLICATION_MP4VTT;\n        }\n      }\n    } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\n      if (codecs != null) {\n        if (codecs.contains(\"cea708\")) {\n          return MimeTypes.APPLICATION_CEA708;\n        } else if (codecs.contains(\"eia608\") || codecs.contains(\"cea608\")) {\n          return MimeTypes.APPLICATION_CEA608;\n        }\n      }\n      return null;\n    }\n    return null;\n  }\n\n  /**\n   * Returns whether a mimeType is a text sample mimeType.\n   *\n   * @param mimeType The mimeType.\n   * @return Whether the mimeType is a text sample mimeType.\n   */\n  private static boolean mimeTypeIsRawText(@Nullable String mimeType) {\n    return MimeTypes.isText(mimeType)\n        || MimeTypes.APPLICATION_TTML.equals(mimeType)\n        || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)\n        || MimeTypes.APPLICATION_CEA708.equals(mimeType)\n        || MimeTypes.APPLICATION_CEA608.equals(mimeType);\n  }\n\n  /**\n   * Checks two languages for consistency, returning the consistent language, or throwing an {@link\n   * IllegalStateException} if the languages are inconsistent.\n   *\n   * <p>Two languages are consistent if they are equal, or if one is null.\n   *\n   * @param firstLanguage The first language.\n   * @param secondLanguage The second language.\n   * @return The consistent language.\n   */\n  @Nullable\n  private static String checkLanguageConsistency(\n      @Nullable String firstLanguage, @Nullable String secondLanguage) {\n    if (firstLanguage == null) {\n      return secondLanguage;\n    } else if (secondLanguage == null) {\n      return firstLanguage;\n    } else {\n      Assertions.checkState(firstLanguage.equals(secondLanguage));\n      return firstLanguage;\n    }\n  }\n\n  /**\n   * Checks two adaptation set content types for consistency, returning the consistent type, or\n   * throwing an {@link IllegalStateException} if the types are inconsistent.\n   * <p>\n   * Two types are consistent if they are equal, or if one is {@link C#TRACK_TYPE_UNKNOWN}.\n   * Where one of the types is {@link C#TRACK_TYPE_UNKNOWN}, the other is returned.\n   *\n   * @param firstType The first type.\n   * @param secondType The second type.\n   * @return The consistent type.\n   */\n  private static int checkContentTypeConsistency(int firstType, int secondType) {\n    if (firstType == C.TRACK_TYPE_UNKNOWN) {\n      return secondType;\n    } else if (secondType == C.TRACK_TYPE_UNKNOWN) {\n      return firstType;\n    } else {\n      Assertions.checkState(firstType == secondType);\n      return firstType;\n    }\n  }\n\n  /**\n   * Parses a {@link Descriptor} from an element.\n   *\n   * @param xpp The parser from which to read.\n   * @param tag The tag of the element being parsed.\n   * @throws XmlPullParserException If an error occurs parsing the element.\n   * @throws IOException If an error occurs reading the element.\n   * @return The parsed {@link Descriptor}.\n   */\n  protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag)\n      throws XmlPullParserException, IOException {\n    String schemeIdUri = parseString(xpp, \"schemeIdUri\", \"\");\n    String value = parseString(xpp, \"value\", null);\n    String id = parseString(xpp, \"id\", null);\n    do {\n      xpp.next();\n    } while (!XmlPullParserUtil.isEndTag(xpp, tag));\n    return new Descriptor(schemeIdUri, value, id);\n  }\n\n  protected static int parseCea608AccessibilityChannel(\n      List<Descriptor> accessibilityDescriptors) {\n    for (int i = 0; i < accessibilityDescriptors.size(); i++) {\n      Descriptor descriptor = accessibilityDescriptors.get(i);\n      if (\"urn:scte:dash:cc:cea-608:2015\".equals(descriptor.schemeIdUri)\n          && descriptor.value != null) {\n        Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value);\n        if (accessibilityValueMatcher.matches()) {\n          return Integer.parseInt(accessibilityValueMatcher.group(1));\n        } else {\n          Log.w(TAG, \"Unable to parse CEA-608 channel number from: \" + descriptor.value);\n        }\n      }\n    }\n    return Format.NO_VALUE;\n  }\n\n  protected static int parseCea708AccessibilityChannel(\n      List<Descriptor> accessibilityDescriptors) {\n    for (int i = 0; i < accessibilityDescriptors.size(); i++) {\n      Descriptor descriptor = accessibilityDescriptors.get(i);\n      if (\"urn:scte:dash:cc:cea-708:2015\".equals(descriptor.schemeIdUri)\n          && descriptor.value != null) {\n        Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value);\n        if (accessibilityValueMatcher.matches()) {\n          return Integer.parseInt(accessibilityValueMatcher.group(1));\n        } else {\n          Log.w(TAG, \"Unable to parse CEA-708 service block number from: \" + descriptor.value);\n        }\n      }\n    }\n    return Format.NO_VALUE;\n  }\n\n  protected static String parseEac3SupplementalProperties(List<Descriptor> supplementalProperties) {\n    for (int i = 0; i < supplementalProperties.size(); i++) {\n      Descriptor descriptor = supplementalProperties.get(i);\n      String schemeIdUri = descriptor.schemeIdUri;\n      if ((\"tag:dolby.com,2018:dash:EC3_ExtensionType:2018\".equals(schemeIdUri)\n              && \"JOC\".equals(descriptor.value))\n          || (\"tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014\".equals(schemeIdUri)\n              && \"ec+3\".equals(descriptor.value))) {\n        return MimeTypes.AUDIO_E_AC3_JOC;\n      }\n    }\n    return MimeTypes.AUDIO_E_AC3;\n  }\n\n  protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) {\n    float frameRate = defaultValue;\n    String frameRateAttribute = xpp.getAttributeValue(null, \"frameRate\");\n    if (frameRateAttribute != null) {\n      Matcher frameRateMatcher = FRAME_RATE_PATTERN.matcher(frameRateAttribute);\n      if (frameRateMatcher.matches()) {\n        int numerator = Integer.parseInt(frameRateMatcher.group(1));\n        String denominatorString = frameRateMatcher.group(2);\n        if (!TextUtils.isEmpty(denominatorString)) {\n          frameRate = (float) numerator / Integer.parseInt(denominatorString);\n        } else {\n          frameRate = numerator;\n        }\n      }\n    }\n    return frameRate;\n  }\n\n  protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) {\n    String value = xpp.getAttributeValue(null, name);\n    if (value == null) {\n      return defaultValue;\n    } else {\n      return Util.parseXsDuration(value);\n    }\n  }\n\n  protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)\n      throws ParserException {\n    String value = xpp.getAttributeValue(null, name);\n    if (value == null) {\n      return defaultValue;\n    } else {\n      return Util.parseXsDateTime(value);\n    }\n  }\n\n  protected static String parseText(XmlPullParser xpp, String label)\n      throws XmlPullParserException, IOException {\n    String text = \"\";\n    do {\n      xpp.next();\n      if (xpp.getEventType() == XmlPullParser.TEXT) {\n        text = xpp.getText();\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, label));\n    return text;\n  }\n\n  protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {\n    String value = xpp.getAttributeValue(null, name);\n    return value == null ? defaultValue : Integer.parseInt(value);\n  }\n\n  protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {\n    String value = xpp.getAttributeValue(null, name);\n    return value == null ? defaultValue : Long.parseLong(value);\n  }\n\n  protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {\n    String value = xpp.getAttributeValue(null, name);\n    return value == null ? defaultValue : value;\n  }\n\n  /**\n   * Parses the number of channels from the value attribute of an AudioElementConfiguration with\n   * schemeIdUri \"tag:dolby.com,2014:dash:audio_channel_configuration:2011\", as defined by table E.5\n   * in ETSI TS 102 366, or the legacy schemeIdUri\n   * \"urn:dolby:dash:audio_channel_configuration:2011\".\n   *\n   * @param xpp The parser from which to read.\n   * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could\n   *     not be parsed.\n   */\n  protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) {\n    String value = Util.toLowerInvariant(xpp.getAttributeValue(null, \"value\"));\n    if (value == null) {\n      return Format.NO_VALUE;\n    }\n    switch (value) {\n      case \"4000\":\n        return 1;\n      case \"a000\":\n        return 2;\n      case \"f801\":\n        return 6;\n      case \"fa01\":\n        return 8;\n      default:\n        return Format.NO_VALUE;\n    }\n  }\n\n  protected static long parseLastSegmentNumberSupplementalProperty(\n      List<Descriptor> supplementalProperties) {\n    for (int i = 0; i < supplementalProperties.size(); i++) {\n      Descriptor descriptor = supplementalProperties.get(i);\n      if (\"http://dashif.org/guidelines/last-segment-number\"\n          .equalsIgnoreCase(descriptor.schemeIdUri)) {\n        return Long.parseLong(descriptor.value);\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /** A parsed Representation element. */\n  protected static final class RepresentationInfo {\n\n    public final Format format;\n    public final String baseUrl;\n    public final SegmentBase segmentBase;\n    @Nullable public final String drmSchemeType;\n    public final ArrayList<SchemeData> drmSchemeDatas;\n    public final ArrayList<Descriptor> inbandEventStreams;\n    public final long revisionId;\n\n    public RepresentationInfo(\n        Format format,\n        String baseUrl,\n        SegmentBase segmentBase,\n        @Nullable String drmSchemeType,\n        ArrayList<SchemeData> drmSchemeDatas,\n        ArrayList<Descriptor> inbandEventStreams,\n        long revisionId) {\n      this.format = format;\n      this.baseUrl = baseUrl;\n      this.segmentBase = segmentBase;\n      this.drmSchemeType = drmSchemeType;\n      this.drmSchemeDatas = drmSchemeDatas;\n      this.inbandEventStreams = inbandEventStreams;\n      this.revisionId = revisionId;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * A descriptor, as defined by ISO 23009-1, 2nd edition, 5.8.2.\n */\npublic final class Descriptor {\n\n  /** The scheme URI. */\n  public final String schemeIdUri;\n  /**\n   * The value, or null.\n   */\n  @Nullable public final String value;\n  /**\n   * The identifier, or null.\n   */\n  @Nullable public final String id;\n\n  /**\n   * @param schemeIdUri The scheme URI.\n   * @param value The value, or null.\n   * @param id The identifier, or null.\n   */\n  public Descriptor(String schemeIdUri, @Nullable String value, @Nullable String id) {\n    this.schemeIdUri = schemeIdUri;\n    this.value = value;\n    this.id = id;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    Descriptor other = (Descriptor) obj;\n    return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value)\n        && Util.areEqual(id, other.id);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = schemeIdUri.hashCode();\n    result = 31 * result + (value != null ? value.hashCode() : 0);\n    result = 31 * result + (id != null ? id.hashCode() : 0);\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\n\n/**\n * A DASH in-MPD EventStream element, as defined by ISO/IEC 23009-1, 2nd edition, section 5.10.\n */\npublic final class EventStream {\n\n  /**\n   * {@link EventMessage}s in the event stream.\n   */\n  public final EventMessage[] events;\n\n  /**\n   * Presentation time of the events in microsecond, sorted in ascending order.\n   */\n  public final long[] presentationTimesUs;\n\n  /**\n   * The scheme URI.\n   */\n  public final String schemeIdUri;\n\n  /**\n   * The value of the event stream. Use empty string if not defined in manifest.\n   */\n  public final String value;\n\n  /**\n   * The timescale in units per seconds, as defined in the manifest.\n   */\n  public final long timescale;\n\n  public EventStream(String schemeIdUri, String value, long timescale, long[] presentationTimesUs,\n      EventMessage[] events) {\n    this.schemeIdUri = schemeIdUri;\n    this.value = value;\n    this.timescale = timescale;\n    this.presentationTimesUs = presentationTimesUs;\n    this.events = events;\n  }\n\n  /**\n   * A constructed id of this {@link EventStream}. Equal to {@code schemeIdUri + \"/\" + value}.\n   */\n  public String id() {\n    return schemeIdUri + \"/\" + value;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Encapsulates media content components over a contiguous period of time.\n */\npublic class Period {\n\n  /**\n   * The period identifier, if one exists.\n   */\n  @Nullable public final String id;\n\n  /**\n   * The start time of the period in milliseconds.\n   */\n  public final long startMs;\n\n  /**\n   * The adaptation sets belonging to the period.\n   */\n  public final List<AdaptationSet> adaptationSets;\n\n  /**\n   * The event stream belonging to the period.\n   */\n  public final List<EventStream> eventStreams;\n\n  /**\n   * @param id The period identifier. May be null.\n   * @param startMs The start time of the period in milliseconds.\n   * @param adaptationSets The adaptation sets belonging to the period.\n   */\n  public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets) {\n    this(id, startMs, adaptationSets, Collections.emptyList());\n  }\n\n  /**\n   * @param id The period identifier. May be null.\n   * @param startMs The start time of the period in milliseconds.\n   * @param adaptationSets The adaptation sets belonging to the period.\n   * @param eventStreams The {@link EventStream}s belonging to the period.\n   */\n  public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets,\n      List<EventStream> eventStreams) {\n    this.id = id;\n    this.startMs = startMs;\n    this.adaptationSets = Collections.unmodifiableList(adaptationSets);\n    this.eventStreams = Collections.unmodifiableList(eventStreams);\n  }\n\n  /**\n   * Returns the index of the first adaptation set of a given type, or {@link C#INDEX_UNSET} if no\n   * adaptation set of the specified type exists.\n   *\n   * @param type An adaptation set type.\n   * @return The index of the first adaptation set of the specified type, or {@link C#INDEX_UNSET}.\n   */\n  public int getAdaptationSetIndex(int type) {\n    int adaptationCount = adaptationSets.size();\n    for (int i = 0; i < adaptationCount; i++) {\n      if (adaptationSets.get(i).type == type) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\n\n/** A parsed program information element. */\npublic class ProgramInformation {\n  /** The title for the media presentation. */\n  @Nullable public final String title;\n\n  /** Information about the original source of the media presentation. */\n  @Nullable public final String source;\n\n  /** A copyright statement for the media presentation. */\n  @Nullable public final String copyright;\n\n  /** A URL that provides more information about the media presentation. */\n  @Nullable public final String moreInformationURL;\n\n  /** Declares the language code(s) for this ProgramInformation. */\n  @Nullable public final String lang;\n\n  public ProgramInformation(\n      @Nullable String title,\n      @Nullable String source,\n      @Nullable String copyright,\n      @Nullable String moreInformationURL,\n      @Nullable String lang) {\n    this.title = title;\n    this.source = source;\n    this.copyright = copyright;\n    this.moreInformationURL = moreInformationURL;\n    this.lang = lang;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    ProgramInformation other = (ProgramInformation) obj;\n    return Util.areEqual(this.title, other.title)\n        && Util.areEqual(this.source, other.source)\n        && Util.areEqual(this.copyright, other.copyright)\n        && Util.areEqual(this.moreInformationURL, other.moreInformationURL)\n        && Util.areEqual(this.lang, other.lang);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 17;\n    result = 31 * result + (title != null ? title.hashCode() : 0);\n    result = 31 * result + (source != null ? source.hashCode() : 0);\n    result = 31 * result + (copyright != null ? copyright.hashCode() : 0);\n    result = 31 * result + (moreInformationURL != null ? moreInformationURL.hashCode() : 0);\n    result = 31 * result + (lang != null ? lang.hashCode() : 0);\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.UriUtil;\n\n/**\n * Defines a range of data located at a reference uri.\n */\npublic final class RangedUri {\n\n  /**\n   * The (zero based) index of the first byte of the range.\n   */\n  public final long start;\n\n  /**\n   * The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is unbounded.\n   */\n  public final long length;\n\n  private final String referenceUri;\n\n  private int hashCode;\n\n  /**\n   * Constructs an ranged uri.\n   *\n   * @param referenceUri The reference uri.\n   * @param start The (zero based) index of the first byte of the range.\n   * @param length The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is\n   *     unbounded.\n   */\n  public RangedUri(@Nullable String referenceUri, long start, long length) {\n    this.referenceUri = referenceUri == null ? \"\" : referenceUri;\n    this.start = start;\n    this.length = length;\n  }\n\n  /**\n   * Returns the resolved {@link Uri} represented by the instance.\n   *\n   * @param baseUri The base Uri.\n   * @return The {@link Uri} represented by the instance.\n   */\n  public Uri resolveUri(String baseUri) {\n    return UriUtil.resolveToUri(baseUri, referenceUri);\n  }\n\n  /**\n   * Returns the resolved uri represented by the instance as a string.\n   *\n   * @param baseUri The base Uri.\n   * @return The uri represented by the instance.\n   */\n  public String resolveUriString(String baseUri) {\n    return UriUtil.resolve(baseUri, referenceUri);\n  }\n\n  /**\n   * Attempts to merge this {@link RangedUri} with another and an optional common base uri.\n   *\n   * <p>A merge is successful if both instances define the same {@link Uri} after resolution with\n   * the base uri, and if one starts the byte after the other ends, forming a contiguous region with\n   * no overlap.\n   *\n   * <p>If {@code other} is null then the merge is considered unsuccessful, and null is returned.\n   *\n   * @param other The {@link RangedUri} to merge.\n   * @param baseUri The base Uri.\n   * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.\n   */\n  @Nullable\n  public RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) {\n    final String resolvedUri = resolveUriString(baseUri);\n    if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) {\n      return null;\n    } else if (length != C.LENGTH_UNSET && start + length == other.start) {\n      return new RangedUri(resolvedUri, start,\n          other.length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length + other.length);\n    } else if (other.length != C.LENGTH_UNSET && other.start + other.length == start) {\n      return new RangedUri(resolvedUri, other.start,\n          length == C.LENGTH_UNSET ? C.LENGTH_UNSET : other.length + length);\n    } else {\n      return null;\n    }\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 17;\n      result = 31 * result + (int) start;\n      result = 31 * result + (int) length;\n      result = 31 * result + referenceUri.hashCode();\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    RangedUri other = (RangedUri) obj;\n    return this.start == other.start\n        && this.length == other.length\n        && referenceUri.equals(other.referenceUri);\n  }\n\n  @Override\n  public String toString() {\n    return \"RangedUri(\"\n        + \"referenceUri=\"\n        + referenceUri\n        + \", start=\"\n        + start\n        + \", length=\"\n        + length\n        + \")\";\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.dash.DashSegmentIndex;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A DASH representation.\n */\npublic abstract class Representation {\n\n  /**\n   * A default value for {@link #revisionId}.\n   */\n  public static final long REVISION_ID_DEFAULT = -1;\n\n  /**\n   * Identifies the revision of the media contained within the representation. If the media can\n   * change over time (e.g. as a result of it being re-encoded), then this identifier can be set to\n   * uniquely identify the revision of the media. The timestamp at which the media was encoded is\n   * often a suitable.\n   */\n  public final long revisionId;\n  /**\n   * The format of the representation.\n   */\n  public final Format format;\n  /**\n   * The base URL of the representation.\n   */\n  public final String baseUrl;\n  /**\n   * The offset of the presentation timestamps in the media stream relative to media time.\n   */\n  public final long presentationTimeOffsetUs;\n  /** The in-band event streams in the representation. May be empty. */\n  public final List<Descriptor> inbandEventStreams;\n\n  private final RangedUri initializationUri;\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param revisionId Identifies the revision of the content.\n   * @param format The format of the representation.\n   * @param baseUrl The base URL.\n   * @param segmentBase A segment base element for the representation.\n   * @return The constructed instance.\n   */\n  public static Representation newInstance(\n      long revisionId, Format format, String baseUrl, SegmentBase segmentBase) {\n    return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param revisionId Identifies the revision of the content.\n   * @param format The format of the representation.\n   * @param baseUrl The base URL.\n   * @param segmentBase A segment base element for the representation.\n   * @param inbandEventStreams The in-band event streams in the representation. May be null.\n   * @return The constructed instance.\n   */\n  public static Representation newInstance(\n      long revisionId,\n      Format format,\n      String baseUrl,\n      SegmentBase segmentBase,\n      @Nullable List<Descriptor> inbandEventStreams) {\n    return newInstance(\n        revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param revisionId Identifies the revision of the content.\n   * @param format The format of the representation.\n   * @param baseUrl The base URL of the representation.\n   * @param segmentBase A segment base element for the representation.\n   * @param inbandEventStreams The in-band event streams in the representation. May be null.\n   * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This\n   *     parameter is ignored if {@code segmentBase} consists of multiple segments.\n   * @return The constructed instance.\n   */\n  public static Representation newInstance(\n      long revisionId,\n      Format format,\n      String baseUrl,\n      SegmentBase segmentBase,\n      @Nullable List<Descriptor> inbandEventStreams,\n      @Nullable String cacheKey) {\n    if (segmentBase instanceof SingleSegmentBase) {\n      return new SingleSegmentRepresentation(\n          revisionId,\n          format,\n          baseUrl,\n          (SingleSegmentBase) segmentBase,\n          inbandEventStreams,\n          cacheKey,\n          C.LENGTH_UNSET);\n    } else if (segmentBase instanceof MultiSegmentBase) {\n      return new MultiSegmentRepresentation(\n          revisionId, format, baseUrl, (MultiSegmentBase) segmentBase, inbandEventStreams);\n    } else {\n      throw new IllegalArgumentException(\"segmentBase must be of type SingleSegmentBase or \"\n          + \"MultiSegmentBase\");\n    }\n  }\n\n  private Representation(\n      long revisionId,\n      Format format,\n      String baseUrl,\n      SegmentBase segmentBase,\n      @Nullable List<Descriptor> inbandEventStreams) {\n    this.revisionId = revisionId;\n    this.format = format;\n    this.baseUrl = baseUrl;\n    this.inbandEventStreams =\n        inbandEventStreams == null\n            ? Collections.emptyList()\n            : Collections.unmodifiableList(inbandEventStreams);\n    initializationUri = segmentBase.getInitialization(this);\n    presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();\n  }\n\n  /**\n   * Returns a {@link RangedUri} defining the location of the representation's initialization data,\n   * or null if no initialization data exists.\n   */\n  @Nullable\n  public RangedUri getInitializationUri() {\n    return initializationUri;\n  }\n\n  /**\n   * Returns a {@link RangedUri} defining the location of the representation's segment index, or\n   * null if the representation provides an index directly.\n   */\n  @Nullable\n  public abstract RangedUri getIndexUri();\n\n  /** Returns an index if the representation provides one directly, or null otherwise. */\n  @Nullable\n  public abstract DashSegmentIndex getIndex();\n\n  /** Returns a cache key for the representation if set, or null. */\n  @Nullable\n  public abstract String getCacheKey();\n\n  /**\n   * A DASH representation consisting of a single segment.\n   */\n  public static class SingleSegmentRepresentation extends Representation {\n\n    /**\n     * The uri of the single segment.\n     */\n    public final Uri uri;\n\n    /**\n     * The content length, or {@link C#LENGTH_UNSET} if unknown.\n     */\n    public final long contentLength;\n\n    @Nullable private final String cacheKey;\n    @Nullable private final RangedUri indexUri;\n    @Nullable private final SingleSegmentIndex segmentIndex;\n\n    /**\n     * @param revisionId Identifies the revision of the content.\n     * @param format The format of the representation.\n     * @param uri The uri of the media.\n     * @param initializationStart The offset of the first byte of initialization data.\n     * @param initializationEnd The offset of the last byte of initialization data.\n     * @param indexStart The offset of the first byte of index data.\n     * @param indexEnd The offset of the last byte of index data.\n     * @param inbandEventStreams The in-band event streams in the representation. May be null.\n     * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null.\n     * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.\n     */\n    public static SingleSegmentRepresentation newInstance(\n        long revisionId,\n        Format format,\n        String uri,\n        long initializationStart,\n        long initializationEnd,\n        long indexStart,\n        long indexEnd,\n        List<Descriptor> inbandEventStreams,\n        @Nullable String cacheKey,\n        long contentLength) {\n      RangedUri rangedUri = new RangedUri(null, initializationStart,\n          initializationEnd - initializationStart + 1);\n      SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart,\n          indexEnd - indexStart + 1);\n      return new SingleSegmentRepresentation(\n          revisionId, format, uri, segmentBase, inbandEventStreams, cacheKey, contentLength);\n    }\n\n    /**\n     * @param revisionId Identifies the revision of the content.\n     * @param format The format of the representation.\n     * @param baseUrl The base URL of the representation.\n     * @param segmentBase The segment base underlying the representation.\n     * @param inbandEventStreams The in-band event streams in the representation. May be null.\n     * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null.\n     * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.\n     */\n    public SingleSegmentRepresentation(\n        long revisionId,\n        Format format,\n        String baseUrl,\n        SingleSegmentBase segmentBase,\n        @Nullable List<Descriptor> inbandEventStreams,\n        @Nullable String cacheKey,\n        long contentLength) {\n      super(revisionId, format, baseUrl, segmentBase, inbandEventStreams);\n      this.uri = Uri.parse(baseUrl);\n      this.indexUri = segmentBase.getIndex();\n      this.cacheKey = cacheKey;\n      this.contentLength = contentLength;\n      // If we have an index uri then the index is defined externally, and we shouldn't return one\n      // directly. If we don't, then we can't do better than an index defining a single segment.\n      segmentIndex = indexUri != null ? null\n          : new SingleSegmentIndex(new RangedUri(null, 0, contentLength));\n    }\n\n    @Override\n    @Nullable\n    public RangedUri getIndexUri() {\n      return indexUri;\n    }\n\n    @Override\n    @Nullable\n    public DashSegmentIndex getIndex() {\n      return segmentIndex;\n    }\n\n    @Override\n    @Nullable\n    public String getCacheKey() {\n      return cacheKey;\n    }\n\n  }\n\n  /**\n   * A DASH representation consisting of multiple segments.\n   */\n  public static class MultiSegmentRepresentation extends Representation\n      implements DashSegmentIndex {\n\n    private final MultiSegmentBase segmentBase;\n\n    /**\n     * @param revisionId Identifies the revision of the content.\n     * @param format The format of the representation.\n     * @param baseUrl The base URL of the representation.\n     * @param segmentBase The segment base underlying the representation.\n     * @param inbandEventStreams The in-band event streams in the representation. May be null.\n     */\n    public MultiSegmentRepresentation(\n        long revisionId,\n        Format format,\n        String baseUrl,\n        MultiSegmentBase segmentBase,\n        @Nullable List<Descriptor> inbandEventStreams) {\n      super(revisionId, format, baseUrl, segmentBase, inbandEventStreams);\n      this.segmentBase = segmentBase;\n    }\n\n    @Override\n    @Nullable\n    public RangedUri getIndexUri() {\n      return null;\n    }\n\n    @Override\n    public DashSegmentIndex getIndex() {\n      return this;\n    }\n\n    @Override\n    @Nullable\n    public String getCacheKey() {\n      return null;\n    }\n\n    // DashSegmentIndex implementation.\n\n    @Override\n    public RangedUri getSegmentUrl(long segmentIndex) {\n      return segmentBase.getSegmentUrl(this, segmentIndex);\n    }\n\n    @Override\n    public long getSegmentNum(long timeUs, long periodDurationUs) {\n      return segmentBase.getSegmentNum(timeUs, periodDurationUs);\n    }\n\n    @Override\n    public long getTimeUs(long segmentIndex) {\n      return segmentBase.getSegmentTimeUs(segmentIndex);\n    }\n\n    @Override\n    public long getDurationUs(long segmentIndex, long periodDurationUs) {\n      return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs);\n    }\n\n    @Override\n    public long getFirstSegmentNum() {\n      return segmentBase.getFirstSegmentNum();\n    }\n\n    @Override\n    public int getSegmentCount(long periodDurationUs) {\n      return segmentBase.getSegmentCount(periodDurationUs);\n    }\n\n    @Override\n    public boolean isExplicit() {\n      return segmentBase.isExplicit();\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.dash.DashSegmentIndex;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\n\n/**\n * An approximate representation of a SegmentBase manifest element.\n */\npublic abstract class SegmentBase {\n\n  /* package */ @Nullable final RangedUri initialization;\n  /* package */ final long timescale;\n  /* package */ final long presentationTimeOffset;\n\n  /**\n   * @param initialization A {@link RangedUri} corresponding to initialization data, if such data\n   *     exists.\n   * @param timescale The timescale in units per second.\n   * @param presentationTimeOffset The presentation time offset. The value in seconds is the\n   *     division of this value and {@code timescale}.\n   */\n  public SegmentBase(\n      @Nullable RangedUri initialization, long timescale, long presentationTimeOffset) {\n    this.initialization = initialization;\n    this.timescale = timescale;\n    this.presentationTimeOffset = presentationTimeOffset;\n  }\n\n  /**\n   * Returns the {@link RangedUri} defining the location of initialization data for a given\n   * representation, or null if no initialization data exists.\n   *\n   * @param representation The {@link Representation} for which initialization data is required.\n   * @return A {@link RangedUri} defining the location of the initialization data, or null.\n   */\n  @Nullable\n  public RangedUri getInitialization(Representation representation) {\n    return initialization;\n  }\n\n  /**\n   * Returns the presentation time offset, in microseconds.\n   */\n  public long getPresentationTimeOffsetUs() {\n    return Util.scaleLargeTimestamp(presentationTimeOffset, C.MICROS_PER_SECOND, timescale);\n  }\n\n  /**\n   * A {@link SegmentBase} that defines a single segment.\n   */\n  public static class SingleSegmentBase extends SegmentBase {\n\n    /* package */ final long indexStart;\n    /* package */ final long indexLength;\n\n    /**\n     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data\n     *     exists.\n     * @param timescale The timescale in units per second.\n     * @param presentationTimeOffset The presentation time offset. The value in seconds is the\n     *     division of this value and {@code timescale}.\n     * @param indexStart The byte offset of the index data in the segment.\n     * @param indexLength The length of the index data in bytes.\n     */\n    public SingleSegmentBase(\n        @Nullable RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long indexStart,\n        long indexLength) {\n      super(initialization, timescale, presentationTimeOffset);\n      this.indexStart = indexStart;\n      this.indexLength = indexLength;\n    }\n\n    public SingleSegmentBase() {\n      this(\n          /* initialization= */ null,\n          /* timescale= */ 1,\n          /* presentationTimeOffset= */ 0,\n          /* indexStart= */ 0,\n          /* indexLength= */ 0);\n    }\n\n    @Nullable\n    public RangedUri getIndex() {\n      return indexLength <= 0\n          ? null\n          : new RangedUri(/* referenceUri= */ null, indexStart, indexLength);\n    }\n\n  }\n\n  /**\n   * A {@link SegmentBase} that consists of multiple segments.\n   */\n  public abstract static class MultiSegmentBase extends SegmentBase {\n\n    /* package */ final long startNumber;\n    /* package */ final long duration;\n    /* package */ @Nullable final List<SegmentTimelineElement> segmentTimeline;\n\n    /**\n     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data\n     *     exists.\n     * @param timescale The timescale in units per second.\n     * @param presentationTimeOffset The presentation time offset. The value in seconds is the\n     *     division of this value and {@code timescale}.\n     * @param startNumber The sequence number of the first segment.\n     * @param duration The duration of each segment in the case of fixed duration segments. The\n     *     value in seconds is the division of this value and {@code timescale}. If {@code\n     *     segmentTimeline} is non-null then this parameter is ignored.\n     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then\n     *     segments are assumed to be of fixed duration as specified by the {@code duration}\n     *     parameter.\n     */\n    public MultiSegmentBase(\n        @Nullable RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long startNumber,\n        long duration,\n        @Nullable List<SegmentTimelineElement> segmentTimeline) {\n      super(initialization, timescale, presentationTimeOffset);\n      this.startNumber = startNumber;\n      this.duration = duration;\n      this.segmentTimeline = segmentTimeline;\n    }\n\n    /** @see DashSegmentIndex#getSegmentNum(long, long) */\n    public long getSegmentNum(long timeUs, long periodDurationUs) {\n      final long firstSegmentNum = getFirstSegmentNum();\n      final long segmentCount = getSegmentCount(periodDurationUs);\n      if (segmentCount == 0) {\n        return firstSegmentNum;\n      }\n      if (segmentTimeline == null) {\n        // All segments are of equal duration (with the possible exception of the last one).\n        long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;\n        long segmentNum = startNumber + timeUs / durationUs;\n        // Ensure we stay within bounds.\n        return segmentNum < firstSegmentNum ? firstSegmentNum\n            : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum\n            : Math.min(segmentNum, firstSegmentNum + segmentCount - 1);\n      } else {\n        // The index cannot be unbounded. Identify the segment using binary search.\n        long lowIndex = firstSegmentNum;\n        long highIndex = firstSegmentNum + segmentCount - 1;\n        while (lowIndex <= highIndex) {\n          long midIndex = lowIndex + (highIndex - lowIndex) / 2;\n          long midTimeUs = getSegmentTimeUs(midIndex);\n          if (midTimeUs < timeUs) {\n            lowIndex = midIndex + 1;\n          } else if (midTimeUs > timeUs) {\n            highIndex = midIndex - 1;\n          } else {\n            return midIndex;\n          }\n        }\n        return lowIndex == firstSegmentNum ? lowIndex : highIndex;\n      }\n    }\n\n    /** @see DashSegmentIndex#getDurationUs(long, long) */\n    public final long getSegmentDurationUs(long sequenceNumber, long periodDurationUs) {\n      if (segmentTimeline != null) {\n        long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration;\n        return (duration * C.MICROS_PER_SECOND) / timescale;\n      } else {\n        int segmentCount = getSegmentCount(periodDurationUs);\n        return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED\n            && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1)\n            ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))\n            : ((duration * C.MICROS_PER_SECOND) / timescale);\n      }\n    }\n\n    /** @see DashSegmentIndex#getTimeUs(long) */\n    public final long getSegmentTimeUs(long sequenceNumber) {\n      long unscaledSegmentTime;\n      if (segmentTimeline != null) {\n        unscaledSegmentTime =\n            segmentTimeline.get((int) (sequenceNumber - startNumber)).startTime\n                - presentationTimeOffset;\n      } else {\n        unscaledSegmentTime = (sequenceNumber - startNumber) * duration;\n      }\n      return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);\n    }\n\n    /**\n     * Returns a {@link RangedUri} defining the location of a segment for the given index in the\n     * given representation.\n     *\n     * @see DashSegmentIndex#getSegmentUrl(long)\n     */\n    public abstract RangedUri getSegmentUrl(Representation representation, long index);\n\n    /** @see DashSegmentIndex#getFirstSegmentNum() */\n    public long getFirstSegmentNum() {\n      return startNumber;\n    }\n\n    /**\n     * @see DashSegmentIndex#getSegmentCount(long)\n     */\n    public abstract int getSegmentCount(long periodDurationUs);\n\n    /**\n     * @see DashSegmentIndex#isExplicit()\n     */\n    public boolean isExplicit() {\n      return segmentTimeline != null;\n    }\n\n  }\n\n  /**\n   * A {@link MultiSegmentBase} that uses a SegmentList to define its segments.\n   */\n  public static class SegmentList extends MultiSegmentBase {\n\n    /* package */ @Nullable final List<RangedUri> mediaSegments;\n\n    /**\n     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data\n     *     exists.\n     * @param timescale The timescale in units per second.\n     * @param presentationTimeOffset The presentation time offset. The value in seconds is the\n     *     division of this value and {@code timescale}.\n     * @param startNumber The sequence number of the first segment.\n     * @param duration The duration of each segment in the case of fixed duration segments. The\n     *     value in seconds is the division of this value and {@code timescale}. If {@code\n     *     segmentTimeline} is non-null then this parameter is ignored.\n     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then\n     *     segments are assumed to be of fixed duration as specified by the {@code duration}\n     *     parameter.\n     * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments.\n     */\n    public SegmentList(\n        RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long startNumber,\n        long duration,\n        @Nullable List<SegmentTimelineElement> segmentTimeline,\n        @Nullable List<RangedUri> mediaSegments) {\n      super(initialization, timescale, presentationTimeOffset, startNumber, duration,\n          segmentTimeline);\n      this.mediaSegments = mediaSegments;\n    }\n\n    @Override\n    public RangedUri getSegmentUrl(Representation representation, long sequenceNumber) {\n      return mediaSegments.get((int) (sequenceNumber - startNumber));\n    }\n\n    @Override\n    public int getSegmentCount(long periodDurationUs) {\n      return mediaSegments.size();\n    }\n\n    @Override\n    public boolean isExplicit() {\n      return true;\n    }\n\n  }\n\n  /**\n   * A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments.\n   */\n  public static class SegmentTemplate extends MultiSegmentBase {\n\n    /* package */ @Nullable final UrlTemplate initializationTemplate;\n    /* package */ @Nullable final UrlTemplate mediaTemplate;\n    /* package */ final long endNumber;\n\n    /**\n     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data\n     *     exists. The value of this parameter is ignored if {@code initializationTemplate} is\n     *     non-null.\n     * @param timescale The timescale in units per second.\n     * @param presentationTimeOffset The presentation time offset. The value in seconds is the\n     *     division of this value and {@code timescale}.\n     * @param startNumber The sequence number of the first segment.\n     * @param endNumber The sequence number of the last segment as specified by the\n     *     SupplementalProperty with schemeIdUri=\"http://dashif.org/guidelines/last-segment-number\",\n     *     or {@link C#INDEX_UNSET}.\n     * @param duration The duration of each segment in the case of fixed duration segments. The\n     *     value in seconds is the division of this value and {@code timescale}. If {@code\n     *     segmentTimeline} is non-null then this parameter is ignored.\n     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then\n     *     segments are assumed to be of fixed duration as specified by the {@code duration}\n     *     parameter.\n     * @param initializationTemplate A template defining the location of initialization data, if\n     *     such data exists. If non-null then the {@code initialization} parameter is ignored. If\n     *     null then {@code initialization} will be used.\n     * @param mediaTemplate A template defining the location of each media segment.\n     */\n    public SegmentTemplate(\n        RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long startNumber,\n        long endNumber,\n        long duration,\n        @Nullable List<SegmentTimelineElement> segmentTimeline,\n        @Nullable UrlTemplate initializationTemplate,\n        @Nullable UrlTemplate mediaTemplate) {\n      super(\n          initialization,\n          timescale,\n          presentationTimeOffset,\n          startNumber,\n          duration,\n          segmentTimeline);\n      this.initializationTemplate = initializationTemplate;\n      this.mediaTemplate = mediaTemplate;\n      this.endNumber = endNumber;\n    }\n\n    @Override\n    @Nullable\n    public RangedUri getInitialization(Representation representation) {\n      if (initializationTemplate != null) {\n        String urlString = initializationTemplate.buildUri(representation.format.id, 0,\n            representation.format.bitrate, 0);\n        return new RangedUri(urlString, 0, C.LENGTH_UNSET);\n      } else {\n        return super.getInitialization(representation);\n      }\n    }\n\n    @Override\n    public RangedUri getSegmentUrl(Representation representation, long sequenceNumber) {\n      long time;\n      if (segmentTimeline != null) {\n        time = segmentTimeline.get((int) (sequenceNumber - startNumber)).startTime;\n      } else {\n        time = (sequenceNumber - startNumber) * duration;\n      }\n      String uriString = mediaTemplate.buildUri(representation.format.id, sequenceNumber,\n          representation.format.bitrate, time);\n      return new RangedUri(uriString, 0, C.LENGTH_UNSET);\n    }\n\n    @Override\n    public int getSegmentCount(long periodDurationUs) {\n      if (segmentTimeline != null) {\n        return segmentTimeline.size();\n      } else if (endNumber != C.INDEX_UNSET) {\n        return (int) (endNumber - startNumber + 1);\n      } else if (periodDurationUs != C.TIME_UNSET) {\n        long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;\n        return (int) Util.ceilDivide(periodDurationUs, durationUs);\n      } else {\n        return DashSegmentIndex.INDEX_UNBOUNDED;\n      }\n    }\n  }\n\n  /**\n   * Represents a timeline segment from the MPD's SegmentTimeline list.\n   */\n  public static class SegmentTimelineElement {\n\n    /* package */ final long startTime;\n    /* package */ final long duration;\n\n    /**\n     * @param startTime The start time of the element. The value in seconds is the division of this\n     *     value and the {@code timescale} of the enclosing element.\n     * @param duration The duration of the element. The value in seconds is the division of this\n     *     value and the {@code timescale} of the enclosing element.\n     */\n    public SegmentTimelineElement(long startTime, long duration) {\n      this.startTime = startTime;\n      this.duration = duration;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object o) {\n      if (this == o) {\n        return true;\n      }\n      if (o == null || getClass() != o.getClass()) {\n        return false;\n      }\n      SegmentTimelineElement that = (SegmentTimelineElement) o;\n      return startTime == that.startTime && duration == that.duration;\n    }\n\n    @Override\n    public int hashCode() {\n      return 31 * (int) startTime + (int) duration;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport com.google.android.exoplayer2.source.dash.DashSegmentIndex;\n\n/**\n * A {@link DashSegmentIndex} that defines a single segment.\n */\n/* package */ final class SingleSegmentIndex implements DashSegmentIndex {\n\n  private final RangedUri uri;\n\n  /**\n   * @param uri A {@link RangedUri} defining the location of the segment data.\n   */\n  public SingleSegmentIndex(RangedUri uri) {\n    this.uri = uri;\n  }\n\n  @Override\n  public long getSegmentNum(long timeUs, long periodDurationUs) {\n    return 0;\n  }\n\n  @Override\n  public long getTimeUs(long segmentNum) {\n    return 0;\n  }\n\n  @Override\n  public long getDurationUs(long segmentNum, long periodDurationUs) {\n    return periodDurationUs;\n  }\n\n  @Override\n  public RangedUri getSegmentUrl(long segmentNum) {\n    return uri;\n  }\n\n  @Override\n  public long getFirstSegmentNum() {\n    return 0;\n  }\n\n  @Override\n  public int getSegmentCount(long periodDurationUs) {\n    return 1;\n  }\n\n  @Override\n  public boolean isExplicit() {\n    return true;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport java.util.Locale;\n\n/**\n * A template from which URLs can be built.\n * <p>\n * URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.\n */\npublic final class UrlTemplate {\n\n  private static final String REPRESENTATION = \"RepresentationID\";\n  private static final String NUMBER = \"Number\";\n  private static final String BANDWIDTH = \"Bandwidth\";\n  private static final String TIME = \"Time\";\n  private static final String ESCAPED_DOLLAR = \"$$\";\n  private static final String DEFAULT_FORMAT_TAG = \"%01d\";\n\n  private static final int REPRESENTATION_ID = 1;\n  private static final int NUMBER_ID = 2;\n  private static final int BANDWIDTH_ID = 3;\n  private static final int TIME_ID = 4;\n\n  private final String[] urlPieces;\n  private final int[] identifiers;\n  private final String[] identifierFormatTags;\n  private final int identifierCount;\n\n  /**\n   * Compile an instance from the provided template string.\n   *\n   * @param template The template.\n   * @return The compiled instance.\n   * @throws IllegalArgumentException If the template string is malformed.\n   */\n  public static UrlTemplate compile(String template) {\n    // These arrays are sizes assuming each of the four possible identifiers will be present at\n    // most once in the template, which seems like a reasonable assumption.\n    String[] urlPieces = new String[5];\n    int[] identifiers = new int[4];\n    String[] identifierFormatTags = new String[4];\n    int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);\n    return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);\n  }\n\n  /**\n   * Internal constructor. Use {@link #compile(String)} to build instances of this class.\n   */\n  private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,\n      int identifierCount) {\n    this.urlPieces = urlPieces;\n    this.identifiers = identifiers;\n    this.identifierFormatTags = identifierFormatTags;\n    this.identifierCount = identifierCount;\n  }\n\n  /**\n   * Constructs a Uri from the template, substituting in the provided arguments.\n   *\n   * <p>Arguments whose corresponding identifiers are not present in the template will be ignored.\n   *\n   * @param representationId The representation identifier.\n   * @param segmentNumber The segment number.\n   * @param bandwidth The bandwidth.\n   * @param time The time as specified by the segment timeline.\n   * @return The built Uri.\n   */\n  public String buildUri(String representationId, long segmentNumber, int bandwidth, long time) {\n    StringBuilder builder = new StringBuilder();\n    for (int i = 0; i < identifierCount; i++) {\n      builder.append(urlPieces[i]);\n      if (identifiers[i] == REPRESENTATION_ID) {\n        builder.append(representationId);\n      } else if (identifiers[i] == NUMBER_ID) {\n        builder.append(String.format(Locale.US, identifierFormatTags[i], segmentNumber));\n      } else if (identifiers[i] == BANDWIDTH_ID) {\n        builder.append(String.format(Locale.US, identifierFormatTags[i], bandwidth));\n      } else if (identifiers[i] == TIME_ID) {\n        builder.append(String.format(Locale.US, identifierFormatTags[i], time));\n      }\n    }\n    builder.append(urlPieces[identifierCount]);\n    return builder.toString();\n  }\n\n  /**\n   * Parses {@code template}, placing the decomposed components into the provided arrays.\n   * <p>\n   * If the return value is N, {@code urlPieces} will contain (N+1) strings that must be\n   * interleaved with N arguments in order to construct a url. The N identifiers that correspond to\n   * the required arguments, together with the tags that define their required formatting, are\n   * returned in {@code identifiers} and {@code identifierFormatTags} respectively.\n   *\n   * @param template The template to parse.\n   * @param urlPieces A holder for pieces of url parsed from the template.\n   * @param identifiers A holder for identifiers parsed from the template.\n   * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.\n   * @return The number of identifiers in the template url.\n   * @throws IllegalArgumentException If the template string is malformed.\n   */\n  private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,\n      String[] identifierFormatTags) {\n    urlPieces[0] = \"\";\n    int templateIndex = 0;\n    int identifierCount = 0;\n    while (templateIndex < template.length()) {\n      int dollarIndex = template.indexOf(\"$\", templateIndex);\n      if (dollarIndex == -1) {\n        urlPieces[identifierCount] += template.substring(templateIndex);\n        templateIndex = template.length();\n      } else if (dollarIndex != templateIndex) {\n        urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);\n        templateIndex = dollarIndex;\n      } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {\n        urlPieces[identifierCount] += \"$\";\n        templateIndex += 2;\n      } else {\n        int secondIndex = template.indexOf(\"$\", templateIndex + 1);\n        String identifier = template.substring(templateIndex + 1, secondIndex);\n        if (identifier.equals(REPRESENTATION)) {\n          identifiers[identifierCount] = REPRESENTATION_ID;\n        } else {\n          int formatTagIndex = identifier.indexOf(\"%0\");\n          String formatTag = DEFAULT_FORMAT_TAG;\n          if (formatTagIndex != -1) {\n            formatTag = identifier.substring(formatTagIndex);\n            // Allowed conversions are decimal integer (which is the only conversion allowed by the\n            // DASH specification) and hexadecimal integer (due to existing content that uses it).\n            // Else we assume that the conversion is missing, and that it should be decimal integer.\n            if (!formatTag.endsWith(\"d\") && !formatTag.endsWith(\"x\")) {\n              formatTag += \"d\";\n            }\n            identifier = identifier.substring(0, formatTagIndex);\n          }\n          switch (identifier) {\n            case NUMBER:\n              identifiers[identifierCount] = NUMBER_ID;\n              break;\n            case BANDWIDTH:\n              identifiers[identifierCount] = BANDWIDTH_ID;\n              break;\n            case TIME:\n              identifiers[identifierCount] = TIME_ID;\n              break;\n            default:\n              throw new IllegalArgumentException(\"Invalid template: \" + template);\n          }\n          identifierFormatTags[identifierCount] = formatTag;\n        }\n        identifierCount++;\n        urlPieces[identifierCount] = \"\";\n        templateIndex = secondIndex + 1;\n      }\n    }\n    return identifierCount;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UtcTimingElement.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.manifest;\n\n/**\n * Represents a UTCTiming element.\n */\npublic final class UtcTimingElement {\n\n  public final String schemeIdUri;\n  public final String value;\n\n  public UtcTimingElement(String schemeIdUri, String value) {\n    this.schemeIdUri = schemeIdUri;\n    this.value = value;\n  }\n\n  @Override\n  public String toString() {\n    return schemeIdUri + \", \" + value;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.dash.manifest;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.dash.offline;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.offline.DownloadException;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.SegmentDownloader;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.dash.DashSegmentIndex;\nimport com.google.android.exoplayer2.source.dash.DashUtil;\nimport com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex;\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A downloader for DASH streams.\n *\n * <p>Example usage:\n *\n * <pre>{@code\n * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);\n * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory(\"ExoPlayer\", null);\n * DownloaderConstructorHelper constructorHelper =\n *     new DownloaderConstructorHelper(cache, factory);\n * // Create a downloader for the first representation of the first adaptation set of the first\n * // period.\n * DashDownloader dashDownloader =\n *     new DashDownloader(\n *         manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);\n * // Perform the download.\n * dashDownloader.download(progressListener);\n * // Access downloaded data using CacheDataSource\n * CacheDataSource cacheDataSource =\n *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);\n * }</pre>\n */\npublic final class DashDownloader extends SegmentDownloader<DashManifest> {\n\n  /**\n   * @param manifestUri The {@link Uri} of the manifest to be downloaded.\n   * @param streamKeys Keys defining which representations in the manifest should be selected for\n   *     download. If empty, all representations are downloaded.\n   * @param constructorHelper A {@link DownloaderConstructorHelper} instance.\n   */\n  public DashDownloader(\n      Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {\n    super(manifestUri, streamKeys, constructorHelper);\n  }\n\n  @Override\n  protected DashManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException {\n    return ParsingLoadable.load(\n        dataSource, new DashManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST);\n  }\n\n  @Override\n  protected List<Segment> getSegments(\n      DataSource dataSource, DashManifest manifest, boolean allowIncompleteList)\n      throws InterruptedException, IOException {\n    ArrayList<Segment> segments = new ArrayList<>();\n    for (int i = 0; i < manifest.getPeriodCount(); i++) {\n      Period period = manifest.getPeriod(i);\n      long periodStartUs = C.msToUs(period.startMs);\n      long periodDurationUs = manifest.getPeriodDurationUs(i);\n      List<AdaptationSet> adaptationSets = period.adaptationSets;\n      for (int j = 0; j < adaptationSets.size(); j++) {\n        addSegmentsForAdaptationSet(\n            dataSource,\n            adaptationSets.get(j),\n            periodStartUs,\n            periodDurationUs,\n            allowIncompleteList,\n            segments);\n      }\n    }\n    return segments;\n  }\n\n  private static void addSegmentsForAdaptationSet(\n      DataSource dataSource,\n      AdaptationSet adaptationSet,\n      long periodStartUs,\n      long periodDurationUs,\n      boolean allowIncompleteList,\n      ArrayList<Segment> out)\n      throws IOException, InterruptedException {\n    for (int i = 0; i < adaptationSet.representations.size(); i++) {\n      Representation representation = adaptationSet.representations.get(i);\n      DashSegmentIndex index;\n      try {\n        index = getSegmentIndex(dataSource, adaptationSet.type, representation);\n        if (index == null) {\n          // Loading succeeded but there was no index.\n          throw new DownloadException(\"Missing segment index\");\n        }\n      } catch (IOException e) {\n        if (!allowIncompleteList) {\n          throw e;\n        }\n        // Generating an incomplete segment list is allowed. Advance to the next representation.\n        continue;\n      }\n\n      int segmentCount = index.getSegmentCount(periodDurationUs);\n      if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {\n        throw new DownloadException(\"Unbounded segment index\");\n      }\n\n      String baseUrl = representation.baseUrl;\n      RangedUri initializationUri = representation.getInitializationUri();\n      if (initializationUri != null) {\n        addSegment(periodStartUs, baseUrl, initializationUri, out);\n      }\n      RangedUri indexUri = representation.getIndexUri();\n      if (indexUri != null) {\n        addSegment(periodStartUs, baseUrl, indexUri, out);\n      }\n      long firstSegmentNum = index.getFirstSegmentNum();\n      long lastSegmentNum = firstSegmentNum + segmentCount - 1;\n      for (long j = firstSegmentNum; j <= lastSegmentNum; j++) {\n        addSegment(periodStartUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j), out);\n      }\n    }\n  }\n\n  private static void addSegment(\n      long startTimeUs, String baseUrl, RangedUri rangedUri, ArrayList<Segment> out) {\n    DataSpec dataSpec =\n        new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, rangedUri.length, null);\n    out.add(new Segment(startTimeUs, dataSpec));\n  }\n\n  private static @Nullable DashSegmentIndex getSegmentIndex(\n      DataSource dataSource, int trackType, Representation representation)\n      throws IOException, InterruptedException {\n    DashSegmentIndex index = representation.getIndex();\n    if (index != null) {\n      return index;\n    }\n    ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, trackType, representation);\n    return seekMap == null\n        ? null\n        : new DashWrappingSegmentIndex(seekMap, representation.presentationTimeOffsetUs);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.dash.offline;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.dash;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceInputStream;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.Key;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.spec.AlgorithmParameterSpec;\nimport java.util.List;\nimport java.util.Map;\nimport javax.crypto.Cipher;\nimport javax.crypto.CipherInputStream;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\n\n/**\n * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with\n * a 128-bit key and PKCS7 padding.\n *\n * <p>Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is\n * designed specifically for reading whole files as defined in an HLS media playlist. For this\n * reason the implementation is private to the HLS package.\n */\n/* package */ class Aes128DataSource implements DataSource {\n\n  private final DataSource upstream;\n  private final byte[] encryptionKey;\n  private final byte[] encryptionIv;\n\n  @Nullable private CipherInputStream cipherInputStream;\n\n  /**\n   * @param upstream The upstream {@link DataSource}.\n   * @param encryptionKey The encryption key.\n   * @param encryptionIv The encryption initialization vector.\n   */\n  public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {\n    this.upstream = upstream;\n    this.encryptionKey = encryptionKey;\n    this.encryptionIv = encryptionIv;\n  }\n\n  @Override\n  public final void addTransferListener(TransferListener transferListener) {\n    upstream.addTransferListener(transferListener);\n  }\n\n  @Override\n  public final long open(DataSpec dataSpec) throws IOException {\n    Cipher cipher;\n    try {\n      cipher = getCipherInstance();\n    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {\n      throw new RuntimeException(e);\n    }\n\n    Key cipherKey = new SecretKeySpec(encryptionKey, \"AES\");\n    AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);\n\n    try {\n      cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);\n    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {\n      throw new RuntimeException(e);\n    }\n\n    DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec);\n    cipherInputStream = new CipherInputStream(inputStream, cipher);\n    inputStream.open();\n\n    return C.LENGTH_UNSET;\n  }\n\n  @Override\n  public final int read(byte[] buffer, int offset, int readLength) throws IOException {\n    Assertions.checkNotNull(cipherInputStream);\n    int bytesRead = cipherInputStream.read(buffer, offset, readLength);\n    if (bytesRead < 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public final Uri getUri() {\n    return upstream.getUri();\n  }\n\n  @Override\n  public final Map<String, List<String>> getResponseHeaders() {\n    return upstream.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (cipherInputStream != null) {\n      cipherInputStream = null;\n      upstream.close();\n    }\n  }\n\n  protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException {\n    return Cipher.getInstance(\"AES/CBC/PKCS7Padding\");\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport com.google.android.exoplayer2.upstream.DataSource;\n\n/**\n * Default implementation of {@link HlsDataSourceFactory}.\n */\npublic final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory {\n\n  private final DataSource.Factory dataSourceFactory;\n\n  /**\n   * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types.\n   */\n  public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) {\n    this.dataSourceFactory = dataSourceFactory;\n  }\n\n  @Override\n  public DataSource createDataSource(int dataType) {\n    return dataSourceFactory.createDataSource();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\nimport com.google.android.exoplayer2.extractor.ts.Ac3Extractor;\nimport com.google.android.exoplayer2.extractor.ts.Ac4Extractor;\nimport com.google.android.exoplayer2.extractor.ts.AdtsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;\nimport com.google.android.exoplayer2.extractor.ts.TsExtractor;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Default {@link HlsExtractorFactory} implementation.\n */\npublic final class DefaultHlsExtractorFactory implements HlsExtractorFactory {\n\n  public static final String AAC_FILE_EXTENSION = \".aac\";\n  public static final String AC3_FILE_EXTENSION = \".ac3\";\n  public static final String EC3_FILE_EXTENSION = \".ec3\";\n  public static final String AC4_FILE_EXTENSION = \".ac4\";\n  public static final String MP3_FILE_EXTENSION = \".mp3\";\n  public static final String MP4_FILE_EXTENSION = \".mp4\";\n  public static final String M4_FILE_EXTENSION_PREFIX = \".m4\";\n  public static final String MP4_FILE_EXTENSION_PREFIX = \".mp4\";\n  public static final String CMF_FILE_EXTENSION_PREFIX = \".cmf\";\n  public static final String VTT_FILE_EXTENSION = \".vtt\";\n  public static final String WEBVTT_FILE_EXTENSION = \".webvtt\";\n\n  @DefaultTsPayloadReaderFactory.Flags private final int payloadReaderFactoryFlags;\n  private final boolean exposeCea608WhenMissingDeclarations;\n\n  /**\n   * Equivalent to {@link #DefaultHlsExtractorFactory(int, boolean) new\n   * DefaultHlsExtractorFactory(payloadReaderFactoryFlags = 0, exposeCea608WhenMissingDeclarations =\n   * true)}\n   */\n  public DefaultHlsExtractorFactory() {\n    this(/* payloadReaderFactoryFlags= */ 0, /* exposeCea608WhenMissingDeclarations */ true);\n  }\n\n  /**\n   * Creates a factory for HLS segment extractors.\n   *\n   * @param payloadReaderFactoryFlags Flags to add when constructing any {@link\n   *     DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code\n   *     payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}.\n   * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should\n   *     expose a CEA-608 track should the master playlist contain no Closed Captions declarations.\n   *     If the master playlist contains any Closed Captions declarations, this flag is ignored.\n   */\n  public DefaultHlsExtractorFactory(\n      int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) {\n    this.payloadReaderFactoryFlags = payloadReaderFactoryFlags;\n    this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations;\n  }\n\n  @Override\n  public Result createExtractor(\n      @Nullable Extractor previousExtractor,\n      Uri uri,\n      Format format,\n      @Nullable List<Format> muxedCaptionFormats,\n      @Nullable DrmInitData drmInitData,\n      TimestampAdjuster timestampAdjuster,\n      Map<String, List<String>> responseHeaders,\n      ExtractorInput extractorInput)\n      throws InterruptedException, IOException {\n\n    if (previousExtractor != null) {\n      // A extractor has already been successfully used. Return one of the same type.\n      if (isReusable(previousExtractor)) {\n        return buildResult(previousExtractor);\n      } else {\n        Result result =\n            buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster);\n        if (result == null) {\n          throw new IllegalArgumentException(\n              \"Unexpected previousExtractor type: \" + previousExtractor.getClass().getSimpleName());\n        }\n      }\n    }\n\n    // Try selecting the extractor by the file extension.\n    Extractor extractorByFileExtension =\n        createExtractorByFileExtension(\n            uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster);\n    extractorInput.resetPeekPosition();\n    if (sniffQuietly(extractorByFileExtension, extractorInput)) {\n      return buildResult(extractorByFileExtension);\n    }\n\n    // We need to manually sniff each known type, without retrying the one selected by file\n    // extension.\n\n    if (!(extractorByFileExtension instanceof WebvttExtractor)) {\n      WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster);\n      if (sniffQuietly(webvttExtractor, extractorInput)) {\n        return buildResult(webvttExtractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof AdtsExtractor)) {\n      AdtsExtractor adtsExtractor = new AdtsExtractor();\n      if (sniffQuietly(adtsExtractor, extractorInput)) {\n        return buildResult(adtsExtractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof Ac3Extractor)) {\n      Ac3Extractor ac3Extractor = new Ac3Extractor();\n      if (sniffQuietly(ac3Extractor, extractorInput)) {\n        return buildResult(ac3Extractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof Ac4Extractor)) {\n      Ac4Extractor ac4Extractor = new Ac4Extractor();\n      if (sniffQuietly(ac4Extractor, extractorInput)) {\n        return buildResult(ac4Extractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof Mp3Extractor)) {\n      Mp3Extractor mp3Extractor =\n          new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);\n      if (sniffQuietly(mp3Extractor, extractorInput)) {\n        return buildResult(mp3Extractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {\n      FragmentedMp4Extractor fragmentedMp4Extractor =\n          createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats);\n      if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {\n        return buildResult(fragmentedMp4Extractor);\n      }\n    }\n\n    if (!(extractorByFileExtension instanceof TsExtractor)) {\n      TsExtractor tsExtractor =\n          createTsExtractor(\n              payloadReaderFactoryFlags,\n              exposeCea608WhenMissingDeclarations,\n              format,\n              muxedCaptionFormats,\n              timestampAdjuster);\n      if (sniffQuietly(tsExtractor, extractorInput)) {\n        return buildResult(tsExtractor);\n      }\n    }\n\n    // Fall back on the extractor created by file extension.\n    return buildResult(extractorByFileExtension);\n  }\n\n  private Extractor createExtractorByFileExtension(\n      Uri uri,\n      Format format,\n      @Nullable List<Format> muxedCaptionFormats,\n      @Nullable DrmInitData drmInitData,\n      TimestampAdjuster timestampAdjuster) {\n    String lastPathSegment = uri.getLastPathSegment();\n    if (lastPathSegment == null) {\n      lastPathSegment = \"\";\n    }\n    if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)\n        || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)\n        || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {\n      return new WebvttExtractor(format.language, timestampAdjuster);\n    } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {\n      return new AdtsExtractor();\n    } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)\n        || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {\n      return new Ac3Extractor();\n    } else if (lastPathSegment.endsWith(AC4_FILE_EXTENSION)) {\n      return new Ac4Extractor();\n    } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {\n      return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);\n    } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)\n        || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)\n        || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)\n        || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {\n      return createFragmentedMp4Extractor(\n          timestampAdjuster, format, drmInitData, muxedCaptionFormats);\n    } else {\n      // For any other file extension, we assume TS format.\n      return createTsExtractor(\n          payloadReaderFactoryFlags,\n          exposeCea608WhenMissingDeclarations,\n          format,\n          muxedCaptionFormats,\n          timestampAdjuster);\n    }\n  }\n\n  private static TsExtractor createTsExtractor(\n      @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags,\n      boolean exposeCea608WhenMissingDeclarations,\n      Format format,\n      @Nullable List<Format> muxedCaptionFormats,\n      TimestampAdjuster timestampAdjuster) {\n    @DefaultTsPayloadReaderFactory.Flags\n    int payloadReaderFactoryFlags =\n        DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM\n            | userProvidedPayloadReaderFactoryFlags;\n    if (muxedCaptionFormats != null) {\n      // The playlist declares closed caption renditions, we should ignore descriptors.\n      payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;\n    } else if (exposeCea608WhenMissingDeclarations) {\n      // The playlist does not provide any closed caption information. We preemptively declare a\n      // closed caption track on channel 0.\n      muxedCaptionFormats =\n          Collections.singletonList(\n              Format.createTextSampleFormat(\n                  /* id= */ null,\n                  MimeTypes.APPLICATION_CEA608,\n                  /* selectionFlags= */ 0,\n                  /* language= */ null));\n    } else {\n      muxedCaptionFormats = Collections.emptyList();\n    }\n    String codecs = format.codecs;\n    if (!TextUtils.isEmpty(codecs)) {\n      // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really\n      // exist. If we know from the codec attribute that they don't exist, then we can\n      // explicitly ignore them even if they're declared.\n      if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {\n        payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;\n      }\n      if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {\n        payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;\n      }\n    }\n\n    return new TsExtractor(\n        TsExtractor.MODE_HLS,\n        timestampAdjuster,\n        new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats));\n  }\n\n  private static FragmentedMp4Extractor createFragmentedMp4Extractor(\n      TimestampAdjuster timestampAdjuster,\n      Format format,\n      @Nullable DrmInitData drmInitData,\n      @Nullable List<Format> muxedCaptionFormats) {\n    // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid\n    // creating a separate EMSG track for every audio track in a video stream.\n    return new FragmentedMp4Extractor(\n        /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,\n        timestampAdjuster,\n        /* sideloadedTrack= */ null,\n        drmInitData,\n        muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());\n  }\n\n  /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */\n  private static boolean isFmp4Variant(Format format) {\n    Metadata metadata = format.metadata;\n    if (metadata == null) {\n      return false;\n    }\n    for (int i = 0; i < metadata.length(); i++) {\n      Metadata.Entry entry = metadata.get(i);\n      if (entry instanceof HlsTrackMetadataEntry) {\n        return !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty();\n      }\n    }\n    return false;\n  }\n\n  @Nullable\n  private static Result buildResultForSameExtractorType(\n      Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) {\n    if (previousExtractor instanceof WebvttExtractor) {\n      return buildResult(new WebvttExtractor(format.language, timestampAdjuster));\n    } else if (previousExtractor instanceof AdtsExtractor) {\n      return buildResult(new AdtsExtractor());\n    } else if (previousExtractor instanceof Ac3Extractor) {\n      return buildResult(new Ac3Extractor());\n    } else if (previousExtractor instanceof Ac4Extractor) {\n      return buildResult(new Ac4Extractor());\n    } else if (previousExtractor instanceof Mp3Extractor) {\n      return buildResult(new Mp3Extractor());\n    } else {\n      return null;\n    }\n  }\n\n  private static Result buildResult(Extractor extractor) {\n    return new Result(\n        extractor,\n        extractor instanceof AdtsExtractor\n            || extractor instanceof Ac3Extractor\n            || extractor instanceof Ac4Extractor\n            || extractor instanceof Mp3Extractor,\n        isReusable(extractor));\n  }\n\n  private static boolean sniffQuietly(Extractor extractor, ExtractorInput input)\n      throws InterruptedException, IOException {\n    boolean result = false;\n    try {\n      result = extractor.sniff(input);\n    } catch (EOFException e) {\n      // Do nothing.\n    } finally {\n      input.resetPeekPosition();\n    }\n    return result;\n  }\n\n  private static boolean isReusable(Extractor previousExtractor) {\n    return previousExtractor instanceof TsExtractor\n        || previousExtractor instanceof FragmentedMp4Extractor;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * LRU cache that holds up to {@code maxSize} full-segment-encryption keys. Which each addition,\n * once the cache's size exceeds {@code maxSize}, the oldest item (according to insertion order) is\n * removed.\n */\n/* package */ final class FullSegmentEncryptionKeyCache {\n\n  private final LinkedHashMap<Uri, byte[]> backingMap;\n\n  public FullSegmentEncryptionKeyCache(int maxSize) {\n    backingMap =\n        new LinkedHashMap<Uri, byte[]>(\n            /* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) {\n          @Override\n          protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> eldest) {\n            return size() > maxSize;\n          }\n        };\n  }\n\n  /**\n   * Returns the {@code encryptionKey} cached against this {@code uri}, or null if {@code uri} is\n   * null or not present in the cache.\n   */\n  @Nullable\n  public byte[] get(@Nullable Uri uri) {\n    if (uri == null) {\n      return null;\n    }\n    return backingMap.get(uri);\n  }\n\n  /**\n   * Inserts an entry into the cache.\n   *\n   * @throws NullPointerException if {@code uri} or {@code encryptionKey} are null.\n   */\n  @Nullable\n  public byte[] put(Uri uri, byte[] encryptionKey) {\n    return backingMap.put(Assertions.checkNotNull(uri), Assertions.checkNotNull(encryptionKey));\n  }\n\n  /**\n   * Returns true if {@code uri} is present in the cache.\n   *\n   * @throws NullPointerException if {@code uri} is null.\n   */\n  public boolean containsUri(Uri uri) {\n    return backingMap.containsKey(Assertions.checkNotNull(uri));\n  }\n\n  /**\n   * Removes {@code uri} from the cache. If {@code uri} was present in the cahce, this returns the\n   * corresponding {@code encryptionKey}, otherwise null.\n   *\n   * @throws NullPointerException if {@code uri} is null.\n   */\n  @Nullable\n  public byte[] remove(Uri uri) {\n    return backingMap.remove(Assertions.checkNotNull(uri));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.BehindLiveWindowException;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;\nimport com.google.android.exoplayer2.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.chunk.DataChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;\nimport com.google.android.exoplayer2.trackselection.BaseTrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** Source of Hls (possibly adaptive) chunks. */\n/* package */ class HlsChunkSource {\n\n  /**\n   * Chunk holder that allows the scheduling of retries.\n   */\n  public static final class HlsChunkHolder {\n\n    public HlsChunkHolder() {\n      clear();\n    }\n\n    /** The chunk to be loaded next. */\n    @Nullable public Chunk chunk;\n\n    /**\n     * Indicates that the end of the stream has been reached.\n     */\n    public boolean endOfStream;\n\n    /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */\n    @Nullable public Uri playlistUrl;\n\n    /**\n     * Clears the holder.\n     */\n    public void clear() {\n      chunk = null;\n      endOfStream = false;\n      playlistUrl = null;\n    }\n\n  }\n\n  /**\n   * The maximum number of keys that the key cache can hold. This value must be 2 or greater in\n   * order to hold initialization segment and media segment keys simultaneously.\n   */\n  private static final int KEY_CACHE_SIZE = 4;\n\n  private final HlsExtractorFactory extractorFactory;\n  private final DataSource mediaDataSource;\n  private final DataSource encryptionDataSource;\n  private final TimestampAdjusterProvider timestampAdjusterProvider;\n  private final Uri[] playlistUrls;\n  private final Format[] playlistFormats;\n  private final HlsPlaylistTracker playlistTracker;\n  private final TrackGroup trackGroup;\n  @Nullable private final List<Format> muxedCaptionFormats;\n  private final FullSegmentEncryptionKeyCache keyCache;\n\n  private boolean isTimestampMaster;\n  private byte[] scratchSpace;\n  @Nullable private IOException fatalError;\n  @Nullable private Uri expectedPlaylistUrl;\n  private boolean independentSegments;\n\n  // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to\n  // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods\n  // in TrackSelection to avoid unexpected behavior.\n  private TrackSelection trackSelection;\n  private long liveEdgeInPeriodTimeUs;\n  private boolean seenExpectedPlaylistError;\n\n  /**\n   * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for\n   *     media chunks.\n   * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists.\n   * @param playlistUrls The {@link Uri}s of the media playlists that can be adapted between by this\n   *     chunk source.\n   * @param playlistFormats The {@link Format Formats} corresponding to the media playlists.\n   * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the\n   *     chunks.\n   * @param mediaTransferListener The transfer listener which should be informed of any media data\n   *     transfers. May be null if no listener is available.\n   * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If multiple\n   *     {@link HlsChunkSource}s are used for a single playback, they should all share the same\n   *     provider.\n   * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption\n   *     information is available in the master playlist.\n   */\n  public HlsChunkSource(\n      HlsExtractorFactory extractorFactory,\n      HlsPlaylistTracker playlistTracker,\n      Uri[] playlistUrls,\n      Format[] playlistFormats,\n      HlsDataSourceFactory dataSourceFactory,\n      @Nullable TransferListener mediaTransferListener,\n      TimestampAdjusterProvider timestampAdjusterProvider,\n      @Nullable List<Format> muxedCaptionFormats) {\n    this.extractorFactory = extractorFactory;\n    this.playlistTracker = playlistTracker;\n    this.playlistUrls = playlistUrls;\n    this.playlistFormats = playlistFormats;\n    this.timestampAdjusterProvider = timestampAdjusterProvider;\n    this.muxedCaptionFormats = muxedCaptionFormats;\n    keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE);\n    scratchSpace = Util.EMPTY_BYTE_ARRAY;\n    liveEdgeInPeriodTimeUs = C.TIME_UNSET;\n    mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA);\n    if (mediaTransferListener != null) {\n      mediaDataSource.addTransferListener(mediaTransferListener);\n    }\n    encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM);\n    trackGroup = new TrackGroup(playlistFormats);\n    int[] initialTrackSelection = new int[playlistUrls.length];\n    for (int i = 0; i < playlistUrls.length; i++) {\n      initialTrackSelection[i] = i;\n    }\n    trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection);\n  }\n\n  /**\n   * If the source is currently having difficulty providing chunks, then this method throws the\n   * underlying error. Otherwise does nothing.\n   *\n   * @throws IOException The underlying error.\n   */\n  public void maybeThrowError() throws IOException {\n    if (fatalError != null) {\n      throw fatalError;\n    }\n    if (expectedPlaylistUrl != null && seenExpectedPlaylistError) {\n      playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl);\n    }\n  }\n\n  /**\n   * Returns the track group exposed by the source.\n   */\n  public TrackGroup getTrackGroup() {\n    return trackGroup;\n  }\n\n  /**\n   * Sets the current track selection.\n   *\n   * @param trackSelection The {@link TrackSelection}.\n   */\n  public void setTrackSelection(TrackSelection trackSelection) {\n    this.trackSelection = trackSelection;\n  }\n\n  /** Returns the current {@link TrackSelection}. */\n  public TrackSelection getTrackSelection() {\n    return trackSelection;\n  }\n\n  /**\n   * Resets the source.\n   */\n  public void reset() {\n    fatalError = null;\n  }\n\n  /**\n   * Sets whether this chunk source is responsible for initializing timestamp adjusters.\n   *\n   * @param isTimestampMaster True if this chunk source is responsible for initializing timestamp\n   *     adjusters.\n   */\n  public void setIsTimestampMaster(boolean isTimestampMaster) {\n    this.isTimestampMaster = isTimestampMaster;\n  }\n\n  /**\n   * Returns the next chunk to load.\n   *\n   * <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream\n   * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available\n   * but the end of the stream has not been reached, {@link HlsChunkHolder#playlistUrl} is set to\n   * contain the {@link Uri} that refers to the playlist that needs refreshing.\n   *\n   * @param playbackPositionUs The current playback position relative to the period start in\n   *     microseconds. If playback of the period to which this chunk source belongs has not yet\n   *     started, the value will be the starting position in the period minus the duration of any\n   *     media in previous periods still to be played.\n   * @param loadPositionUs The current load position relative to the period start in microseconds.\n   * @param queue The queue of buffered {@link HlsMediaChunk}s.\n   * @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for\n   *     non-empty media playlists. If {@code false}, the last available chunk is returned instead.\n   *     If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set.\n   * @param out A holder to populate.\n   */\n  public void getNextChunk(\n      long playbackPositionUs,\n      long loadPositionUs,\n      List<HlsMediaChunk> queue,\n      boolean allowEndOfStream,\n      HlsChunkHolder out) {\n    HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);\n    int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);\n    long bufferedDurationUs = loadPositionUs - playbackPositionUs;\n    long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);\n    if (previous != null && !independentSegments) {\n      // Unless segments are known to be independent, switching tracks requires downloading\n      // overlapping segments. Hence we subtract the previous segment's duration from the buffered\n      // duration.\n      // This may affect the live-streaming adaptive track selection logic, when we compare the\n      // buffered duration to time-to-live-edge to decide whether to switch. Therefore, we subtract\n      // the duration of the last loaded segment from timeToLiveEdgeUs as well.\n      long subtractedDurationUs = previous.getDurationUs();\n      bufferedDurationUs = Math.max(0, bufferedDurationUs - subtractedDurationUs);\n      if (timeToLiveEdgeUs != C.TIME_UNSET) {\n        timeToLiveEdgeUs = Math.max(0, timeToLiveEdgeUs - subtractedDurationUs);\n      }\n    }\n\n    // Select the track.\n    MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);\n    trackSelection.updateSelectedTrack(\n        playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);\n    int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup();\n\n    boolean switchingTrack = oldTrackIndex != selectedTrackIndex;\n    Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex];\n    if (!playlistTracker.isSnapshotValid(selectedPlaylistUrl)) {\n      out.playlistUrl = selectedPlaylistUrl;\n      seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);\n      expectedPlaylistUrl = selectedPlaylistUrl;\n      // Retry when playlist is refreshed.\n      return;\n    }\n    HlsMediaPlaylist mediaPlaylist =\n        playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);\n    // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null.\n    Assertions.checkNotNull(mediaPlaylist);\n    independentSegments = mediaPlaylist.hasIndependentSegments;\n\n    updateLiveEdgeTimeUs(mediaPlaylist);\n\n    // Select the chunk.\n    long startOfPlaylistInPeriodUs =\n        mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();\n    long chunkMediaSequence =\n        getChunkMediaSequence(\n            previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);\n    if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) {\n        // We try getting the next chunk without adapting in case that's the reason for falling\n        // behind the live window.\n        selectedTrackIndex = oldTrackIndex;\n        selectedPlaylistUrl = playlistUrls[selectedTrackIndex];\n      mediaPlaylist =\n          playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);\n      // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be\n      // non-null.\n      Assertions.checkNotNull(mediaPlaylist);\n        startOfPlaylistInPeriodUs =\n            mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();\n        chunkMediaSequence = previous.getNextChunkIndex();\n    }\n\n    if (chunkMediaSequence < mediaPlaylist.mediaSequence) {\n      fatalError = new BehindLiveWindowException();\n      return;\n    }\n\n    int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);\n    int availableSegmentCount = mediaPlaylist.segments.size();\n    if (segmentIndexInPlaylist >= availableSegmentCount) {\n      if (mediaPlaylist.hasEndTag) {\n        if (allowEndOfStream || availableSegmentCount == 0) {\n          out.endOfStream = true;\n          return;\n        }\n        segmentIndexInPlaylist = availableSegmentCount - 1;\n      } else /* Live */ {\n        out.playlistUrl = selectedPlaylistUrl;\n        seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);\n        expectedPlaylistUrl = selectedPlaylistUrl;\n        return;\n      }\n    }\n    // We have a valid playlist snapshot, we can discard any playlist errors at this point.\n    seenExpectedPlaylistError = false;\n    expectedPlaylistUrl = null;\n\n    // Handle encryption.\n    HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);\n\n    // Check if the segment or its initialization segment are fully encrypted.\n    Uri initSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment.initializationSegment);\n    out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);\n    if (out.chunk != null) {\n      return;\n    }\n    Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment);\n    out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);\n    if (out.chunk != null) {\n      return;\n    }\n\n    out.chunk =\n        HlsMediaChunk.createInstance(\n            extractorFactory,\n            mediaDataSource,\n            playlistFormats[selectedTrackIndex],\n            startOfPlaylistInPeriodUs,\n            mediaPlaylist,\n            segmentIndexInPlaylist,\n            selectedPlaylistUrl,\n            muxedCaptionFormats,\n            trackSelection.getSelectionReason(),\n            trackSelection.getSelectionData(),\n            isTimestampMaster,\n            timestampAdjusterProvider,\n            previous,\n            /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),\n            /* initSegmentKey= */ keyCache.get(initSegmentKeyUri));\n  }\n\n  /**\n   * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this\n   * source.\n   *\n   * @param chunk The chunk whose load has been completed.\n   */\n  public void onChunkLoadCompleted(Chunk chunk) {\n    if (chunk instanceof EncryptionKeyChunk) {\n      EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;\n      scratchSpace = encryptionKeyChunk.getDataHolder();\n      keyCache.put(\n          encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult()));\n    }\n  }\n\n  /**\n   * Attempts to blacklist the track associated with the given chunk. Blacklisting will fail if the\n   * track is the only non-blacklisted track in the selection.\n   *\n   * @param chunk The chunk whose load caused the blacklisting attempt.\n   * @param blacklistDurationMs The number of milliseconds for which the track selection should be\n   *     blacklisted.\n   * @return Whether the blacklisting succeeded.\n   */\n  public boolean maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) {\n    return trackSelection.blacklist(\n        trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs);\n  }\n\n  /**\n   * Called when a playlist load encounters an error.\n   *\n   * @param playlistUrl The {@link Uri} of the playlist whose load encountered an error.\n   * @param blacklistDurationMs The duration for which the playlist should be blacklisted. Or {@link\n   *     C#TIME_UNSET} if the playlist should not be blacklisted.\n   * @return True if blacklisting did not encounter errors. False otherwise.\n   */\n  public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) {\n    int trackGroupIndex = C.INDEX_UNSET;\n    for (int i = 0; i < playlistUrls.length; i++) {\n      if (playlistUrls[i].equals(playlistUrl)) {\n        trackGroupIndex = i;\n        break;\n      }\n    }\n    if (trackGroupIndex == C.INDEX_UNSET) {\n      return true;\n    }\n    int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);\n    if (trackSelectionIndex == C.INDEX_UNSET) {\n      return true;\n    }\n    seenExpectedPlaylistError |= playlistUrl.equals(expectedPlaylistUrl);\n    return blacklistDurationMs == C.TIME_UNSET\n        || trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);\n  }\n\n  /**\n   * Returns an array of {@link MediaChunkIterator}s for upcoming media chunks.\n   *\n   * @param previous The previous media chunk. May be null.\n   * @param loadPositionUs The position at which the iterators will start.\n   * @return Array of {@link MediaChunkIterator}s for each track.\n   */\n  public MediaChunkIterator[] createMediaChunkIterators(\n      @Nullable HlsMediaChunk previous, long loadPositionUs) {\n    int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);\n    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];\n    for (int i = 0; i < chunkIterators.length; i++) {\n      int trackIndex = trackSelection.getIndexInTrackGroup(i);\n      Uri playlistUrl = playlistUrls[trackIndex];\n      if (!playlistTracker.isSnapshotValid(playlistUrl)) {\n        chunkIterators[i] = MediaChunkIterator.EMPTY;\n        continue;\n      }\n      HlsMediaPlaylist playlist =\n          playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false);\n      // Playlist snapshot is valid (checked by if() above) so playlist must be non-null.\n      Assertions.checkNotNull(playlist);\n      long startOfPlaylistInPeriodUs =\n          playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();\n      boolean switchingTrack = trackIndex != oldTrackIndex;\n      long chunkMediaSequence =\n          getChunkMediaSequence(\n              previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);\n      if (chunkMediaSequence < playlist.mediaSequence) {\n        chunkIterators[i] = MediaChunkIterator.EMPTY;\n        continue;\n      }\n      int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence);\n      chunkIterators[i] =\n          new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex);\n    }\n    return chunkIterators;\n  }\n\n  // Private methods.\n\n  /**\n   * Returns the media sequence number of the segment to load next in {@code mediaPlaylist}.\n   *\n   * @param previous The last (at least partially) loaded segment.\n   * @param switchingTrack Whether the segment to load is not preceded by a segment in the same\n   *     track.\n   * @param mediaPlaylist The media playlist to which the segment to load belongs.\n   * @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period\n   *     start in microseconds.\n   * @param loadPositionUs The current load position relative to the period start in microseconds.\n   * @return The media sequence of the segment to load.\n   */\n  private long getChunkMediaSequence(\n      @Nullable HlsMediaChunk previous,\n      boolean switchingTrack,\n      HlsMediaPlaylist mediaPlaylist,\n      long startOfPlaylistInPeriodUs,\n      long loadPositionUs) {\n    if (previous == null || switchingTrack) {\n      long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs;\n      long targetPositionInPeriodUs =\n          (previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;\n      if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {\n        // If the playlist is too old to contain the chunk, we need to refresh it.\n        return mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();\n      }\n      long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;\n      return Util.binarySearchFloor(\n              mediaPlaylist.segments,\n              /* value= */ targetPositionInPlaylistUs,\n              /* inclusive= */ true,\n              /* stayInBounds= */ !playlistTracker.isLive() || previous == null)\n          + mediaPlaylist.mediaSequence;\n    }\n    // We ignore the case of previous not having loaded completely, in which case we load the next\n    // segment.\n    return previous.getNextChunkIndex();\n  }\n\n  private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {\n    final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET;\n    return resolveTimeToLiveEdgePossible\n        ? liveEdgeInPeriodTimeUs - playbackPositionUs\n        : C.TIME_UNSET;\n  }\n\n  private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) {\n    liveEdgeInPeriodTimeUs =\n        mediaPlaylist.hasEndTag\n            ? C.TIME_UNSET\n            : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs());\n  }\n\n  @Nullable\n  private Chunk maybeCreateEncryptionChunkFor(@Nullable Uri keyUri, int selectedTrackIndex) {\n    if (keyUri == null) {\n      return null;\n    }\n\n    byte[] encryptionKey = keyCache.remove(keyUri);\n    if (encryptionKey != null) {\n      // The key was present in the key cache. We re-insert it to prevent it from being evicted by\n      // the following key addition. Note that removal of the key is necessary to affect the\n      // eviction order.\n      keyCache.put(keyUri, encryptionKey);\n      return null;\n    }\n    DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);\n    return new EncryptionKeyChunk(\n        encryptionDataSource,\n        dataSpec,\n        playlistFormats[selectedTrackIndex],\n        trackSelection.getSelectionReason(),\n        trackSelection.getSelectionData(),\n        scratchSpace);\n  }\n\n  @Nullable\n  private static Uri getFullEncryptionKeyUri(HlsMediaPlaylist playlist, @Nullable Segment segment) {\n    if (segment == null || segment.fullSegmentEncryptionKeyUri == null) {\n      return null;\n    }\n    return UriUtil.resolveToUri(playlist.baseUri, segment.fullSegmentEncryptionKeyUri);\n  }\n\n  // Private classes.\n\n  /**\n   * A {@link TrackSelection} to use for initialization.\n   */\n  private static final class InitializationTrackSelection extends BaseTrackSelection {\n\n    private int selectedIndex;\n\n    public InitializationTrackSelection(TrackGroup group, int[] tracks) {\n      super(group, tracks);\n      selectedIndex = indexOf(group.getFormat(0));\n    }\n\n    @Override\n    public void updateSelectedTrack(\n        long playbackPositionUs,\n        long bufferedDurationUs,\n        long availableDurationUs,\n        List<? extends MediaChunk> queue,\n        MediaChunkIterator[] mediaChunkIterators) {\n      long nowMs = SystemClock.elapsedRealtime();\n      if (!isBlacklisted(selectedIndex, nowMs)) {\n        return;\n      }\n      // Try from lowest bitrate to highest.\n      for (int i = length - 1; i >= 0; i--) {\n        if (!isBlacklisted(i, nowMs)) {\n          selectedIndex = i;\n          return;\n        }\n      }\n      // Should never happen.\n      throw new IllegalStateException();\n    }\n\n    @Override\n    public int getSelectedIndex() {\n      return selectedIndex;\n    }\n\n    @Override\n    public int getSelectionReason() {\n      return C.SELECTION_REASON_UNKNOWN;\n    }\n\n    @Override\n    @Nullable\n    public Object getSelectionData() {\n      return null;\n    }\n\n  }\n\n  private static final class EncryptionKeyChunk extends DataChunk {\n\n    private byte @MonotonicNonNull [] result;\n\n    public EncryptionKeyChunk(\n        DataSource dataSource,\n        DataSpec dataSpec,\n        Format trackFormat,\n        int trackSelectionReason,\n        @Nullable Object trackSelectionData,\n        byte[] scratchSpace) {\n      super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason,\n          trackSelectionData, scratchSpace);\n    }\n\n    @Override\n    protected void consume(byte[] data, int limit) {\n      result = Arrays.copyOf(data, limit);\n    }\n\n    /** Return the result of this chunk, or null if loading is not complete. */\n    @Nullable\n    public byte[] getResult() {\n      return result;\n    }\n\n  }\n\n  /** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */\n  private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {\n\n    private final HlsMediaPlaylist playlist;\n    private final long startOfPlaylistInPeriodUs;\n\n    /**\n     * Creates iterator.\n     *\n     * @param playlist The {@link HlsMediaPlaylist} to wrap.\n     * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in\n     *     microseconds.\n     * @param chunkIndex The index of the first available chunk in the playlist.\n     */\n    public HlsMediaPlaylistSegmentIterator(\n        HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) {\n      super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1);\n      this.playlist = playlist;\n      this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs;\n    }\n\n    @Override\n    public DataSpec getDataSpec() {\n      checkInBounds();\n      Segment segment = playlist.segments.get((int) getCurrentIndex());\n      Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url);\n      return new DataSpec(\n          chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null);\n    }\n\n    @Override\n    public long getChunkStartTimeUs() {\n      checkInBounds();\n      Segment segment = playlist.segments.get((int) getCurrentIndex());\n      return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;\n    }\n\n    @Override\n    public long getChunkEndTimeUs() {\n      checkInBounds();\n      Segment segment = playlist.segments.get((int) getCurrentIndex());\n      long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;\n      return segmentStartTimeInPeriodUs + segment.durationUs;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\n\n/**\n * Creates {@link DataSource}s for HLS playlists, encryption and media chunks.\n */\npublic interface HlsDataSourceFactory {\n\n  /**\n   * Creates a {@link DataSource} for the given data type.\n   *\n   * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C}\n   *     {@code .DATA_TYPE_*} constants.\n   * @return A {@link DataSource} for the given data type.\n   */\n  DataSource createDataSource(int dataType);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Factory for HLS media chunk extractors.\n */\npublic interface HlsExtractorFactory {\n\n  /** Holds an {@link Extractor} and associated parameters. */\n  final class Result {\n\n    /** The created extractor; */\n    public final Extractor extractor;\n    /** Whether the segments for which {@link #extractor} is created are packed audio segments. */\n    public final boolean isPackedAudioExtractor;\n    /**\n     * Whether {@link #extractor} may be reused for following continuous (no immediately preceding\n     * discontinuities) segments of the same variant.\n     */\n    public final boolean isReusable;\n\n    /**\n     * Creates a result.\n     *\n     * @param extractor See {@link #extractor}.\n     * @param isPackedAudioExtractor See {@link #isPackedAudioExtractor}.\n     * @param isReusable See {@link #isReusable}.\n     */\n    public Result(Extractor extractor, boolean isPackedAudioExtractor, boolean isReusable) {\n      this.extractor = extractor;\n      this.isPackedAudioExtractor = isPackedAudioExtractor;\n      this.isReusable = isReusable;\n    }\n  }\n\n  HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory();\n\n  /**\n   * Creates an {@link Extractor} for extracting HLS media chunks.\n   *\n   * @param previousExtractor A previously used {@link Extractor} which can be reused if the current\n   *     chunk is a continuation of the previously extracted chunk, or null otherwise. It is the\n   *     responsibility of implementers to only reuse extractors that are suited for reusage.\n   * @param uri The URI of the media chunk.\n   * @param format A {@link Format} associated with the chunk to extract.\n   * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption\n   *     information is available in the master playlist.\n   * @param drmInitData {@link DrmInitData} associated with the chunk.\n   * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.\n   * @param responseHeaders The HTTP response headers associated with the media segment or\n   *     initialization section to extract.\n   * @param sniffingExtractorInput The first extractor input that will be passed to the returned\n   *     extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to\n   *     call {@link Extractor#sniff(ExtractorInput)}.\n   * @return A {@link Result}.\n   * @throws InterruptedException If the thread is interrupted while sniffing.\n   * @throws IOException If an I/O error is encountered while sniffing.\n   */\n  Result createExtractor(\n      @Nullable Extractor previousExtractor,\n      Uri uri,\n      Format format,\n      @Nullable List<Format> muxedCaptionFormats,\n      @Nullable DrmInitData drmInitData,\n      TimestampAdjuster timestampAdjuster,\n      Map<String, List<String>> responseHeaders,\n      ExtractorInput sniffingExtractorInput)\n      throws InterruptedException, IOException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;\n\n/**\n * Holds a master playlist along with a snapshot of one of its media playlists.\n */\npublic final class HlsManifest {\n\n  /**\n   * The master playlist of an HLS stream.\n   */\n  public final HlsMasterPlaylist masterPlaylist;\n  /**\n   * A snapshot of a media playlist referred to by {@link #masterPlaylist}.\n   */\n  public final HlsMediaPlaylist mediaPlaylist;\n\n  /**\n   * @param masterPlaylist The master playlist.\n   * @param mediaPlaylist The media playlist.\n   */\n  HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) {\n    this.masterPlaylist = masterPlaylist;\n    this.mediaPlaylist = mediaPlaylist;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorInput;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder;\nimport com.google.android.exoplayer2.metadata.id3.PrivFrame;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * An HLS {@link MediaChunk}.\n */\n/* package */ final class HlsMediaChunk extends MediaChunk {\n\n  /**\n   * Creates a new instance.\n   *\n   * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk extractor\n   *     is obtained.\n   * @param dataSource The source from which the data should be loaded.\n   * @param format The chunk format.\n   * @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.\n   * @param mediaPlaylist The media playlist from which this chunk was obtained.\n   * @param playlistUrl The url of the playlist from which this chunk was obtained.\n   * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption\n   *     information is available in the master playlist.\n   * @param trackSelectionReason See {@link #trackSelectionReason}.\n   * @param trackSelectionData See {@link #trackSelectionData}.\n   * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.\n   * @param timestampAdjusterProvider The provider from which to obtain the {@link\n   *     TimestampAdjuster}.\n   * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.\n   * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.\n   * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null\n   *     otherwise.\n   */\n  public static HlsMediaChunk createInstance(\n      HlsExtractorFactory extractorFactory,\n      DataSource dataSource,\n      Format format,\n      long startOfPlaylistInPeriodUs,\n      HlsMediaPlaylist mediaPlaylist,\n      int segmentIndexInPlaylist,\n      Uri playlistUrl,\n      @Nullable List<Format> muxedCaptionFormats,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      boolean isMasterTimestampSource,\n      TimestampAdjusterProvider timestampAdjusterProvider,\n      @Nullable HlsMediaChunk previousChunk,\n      @Nullable byte[] mediaSegmentKey,\n      @Nullable byte[] initSegmentKey) {\n    // Media segment.\n    HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);\n    DataSpec dataSpec =\n        new DataSpec(\n            UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),\n            mediaSegment.byterangeOffset,\n            mediaSegment.byterangeLength,\n            /* key= */ null);\n    boolean mediaSegmentEncrypted = mediaSegmentKey != null;\n    byte[] mediaSegmentIv =\n        mediaSegmentEncrypted\n            ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV))\n            : null;\n    DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv);\n\n    // Init segment.\n    HlsMediaPlaylist.Segment initSegment = mediaSegment.initializationSegment;\n    DataSpec initDataSpec = null;\n    boolean initSegmentEncrypted = false;\n    DataSource initDataSource = null;\n    if (initSegment != null) {\n      initSegmentEncrypted = initSegmentKey != null;\n      byte[] initSegmentIv =\n          initSegmentEncrypted\n              ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV))\n              : null;\n      Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);\n      initDataSpec =\n          new DataSpec(\n              initSegmentUri,\n              initSegment.byterangeOffset,\n              initSegment.byterangeLength,\n              /* key= */ null);\n      initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);\n    }\n\n    long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + mediaSegment.relativeStartTimeUs;\n    long segmentEndTimeInPeriodUs = segmentStartTimeInPeriodUs + mediaSegment.durationUs;\n    int discontinuitySequenceNumber =\n        mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence;\n\n    Extractor previousExtractor = null;\n    Id3Decoder id3Decoder;\n    ParsableByteArray scratchId3Data;\n    boolean shouldSpliceIn;\n    if (previousChunk != null) {\n      id3Decoder = previousChunk.id3Decoder;\n      scratchId3Data = previousChunk.scratchId3Data;\n      shouldSpliceIn =\n          !playlistUrl.equals(previousChunk.playlistUrl) || !previousChunk.loadCompleted;\n      previousExtractor =\n          previousChunk.isExtractorReusable\n                  && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber\n                  && !shouldSpliceIn\n              ? previousChunk.extractor\n              : null;\n    } else {\n      id3Decoder = new Id3Decoder();\n      scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);\n      shouldSpliceIn = false;\n    }\n\n    return new HlsMediaChunk(\n        extractorFactory,\n        mediaDataSource,\n        dataSpec,\n        format,\n        mediaSegmentEncrypted,\n        initDataSource,\n        initDataSpec,\n        initSegmentEncrypted,\n        playlistUrl,\n        muxedCaptionFormats,\n        trackSelectionReason,\n        trackSelectionData,\n        segmentStartTimeInPeriodUs,\n        segmentEndTimeInPeriodUs,\n        /* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,\n        discontinuitySequenceNumber,\n        mediaSegment.hasGapTag,\n        isMasterTimestampSource,\n        /* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber),\n        mediaSegment.drmInitData,\n        previousExtractor,\n        id3Decoder,\n        scratchId3Data,\n        shouldSpliceIn);\n  }\n\n  public static final String PRIV_TIMESTAMP_FRAME_OWNER =\n      \"com.apple.streaming.transportStreamTimestamp\";\n  private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder();\n\n  private static final AtomicInteger uidSource = new AtomicInteger();\n\n  /**\n   * A unique identifier for the chunk.\n   */\n  public final int uid;\n\n  /**\n   * The discontinuity sequence number of the chunk.\n   */\n  public final int discontinuitySequenceNumber;\n\n  /** The url of the playlist from which this chunk was obtained. */\n  public final Uri playlistUrl;\n\n  @Nullable private final DataSource initDataSource;\n  @Nullable private final DataSpec initDataSpec;\n  @Nullable private final Extractor previousExtractor;\n\n  private final boolean isMasterTimestampSource;\n  private final boolean hasGapTag;\n  private final TimestampAdjuster timestampAdjuster;\n  private final boolean shouldSpliceIn;\n  private final HlsExtractorFactory extractorFactory;\n  @Nullable private final List<Format> muxedCaptionFormats;\n  @Nullable private final DrmInitData drmInitData;\n  private final Id3Decoder id3Decoder;\n  private final ParsableByteArray scratchId3Data;\n  private final boolean mediaSegmentEncrypted;\n  private final boolean initSegmentEncrypted;\n\n  @MonotonicNonNull private Extractor extractor;\n  private boolean isExtractorReusable;\n  @MonotonicNonNull private HlsSampleStreamWrapper output;\n  // nextLoadPosition refers to the init segment if initDataLoadRequired is true.\n  // Otherwise, nextLoadPosition refers to the media segment.\n  private int nextLoadPosition;\n  private boolean initDataLoadRequired;\n  private volatile boolean loadCanceled;\n  private boolean loadCompleted;\n\n  private HlsMediaChunk(\n      HlsExtractorFactory extractorFactory,\n      DataSource mediaDataSource,\n      DataSpec dataSpec,\n      Format format,\n      boolean mediaSegmentEncrypted,\n      @Nullable DataSource initDataSource,\n      @Nullable DataSpec initDataSpec,\n      boolean initSegmentEncrypted,\n      Uri playlistUrl,\n      @Nullable List<Format> muxedCaptionFormats,\n      int trackSelectionReason,\n      @Nullable Object trackSelectionData,\n      long startTimeUs,\n      long endTimeUs,\n      long chunkMediaSequence,\n      int discontinuitySequenceNumber,\n      boolean hasGapTag,\n      boolean isMasterTimestampSource,\n      TimestampAdjuster timestampAdjuster,\n      @Nullable DrmInitData drmInitData,\n      @Nullable Extractor previousExtractor,\n      Id3Decoder id3Decoder,\n      ParsableByteArray scratchId3Data,\n      boolean shouldSpliceIn) {\n    super(\n        mediaDataSource,\n        dataSpec,\n        format,\n        trackSelectionReason,\n        trackSelectionData,\n        startTimeUs,\n        endTimeUs,\n        chunkMediaSequence);\n    this.mediaSegmentEncrypted = mediaSegmentEncrypted;\n    this.discontinuitySequenceNumber = discontinuitySequenceNumber;\n    this.initDataSpec = initDataSpec;\n    this.initDataSource = initDataSource;\n    this.initDataLoadRequired = initDataSpec != null;\n    this.initSegmentEncrypted = initSegmentEncrypted;\n    this.playlistUrl = playlistUrl;\n    this.isMasterTimestampSource = isMasterTimestampSource;\n    this.timestampAdjuster = timestampAdjuster;\n    this.hasGapTag = hasGapTag;\n    this.extractorFactory = extractorFactory;\n    this.muxedCaptionFormats = muxedCaptionFormats;\n    this.drmInitData = drmInitData;\n    this.previousExtractor = previousExtractor;\n    this.id3Decoder = id3Decoder;\n    this.scratchId3Data = scratchId3Data;\n    this.shouldSpliceIn = shouldSpliceIn;\n    uid = uidSource.getAndIncrement();\n  }\n\n  /**\n   * Initializes the chunk for loading, setting the {@link HlsSampleStreamWrapper} that will receive\n   * samples as they are loaded.\n   *\n   * @param output The output that will receive the loaded samples.\n   */\n  public void init(HlsSampleStreamWrapper output) {\n    this.output = output;\n  }\n\n  @Override\n  public boolean isLoadCompleted() {\n    return loadCompleted;\n  }\n\n  // Loadable implementation\n\n  @Override\n  public void cancelLoad() {\n    loadCanceled = true;\n  }\n\n  @Override\n  public void load() throws IOException, InterruptedException {\n    // output == null means init() hasn't been called.\n    Assertions.checkNotNull(output);\n    if (extractor == null && previousExtractor != null) {\n      extractor = previousExtractor;\n      isExtractorReusable = true;\n      initDataLoadRequired = false;\n      output.init(uid, shouldSpliceIn, /* reusingExtractor= */ true);\n    }\n    maybeLoadInitData();\n    if (!loadCanceled) {\n      if (!hasGapTag) {\n        loadMedia();\n      }\n      loadCompleted = true;\n    }\n  }\n\n  // Internal methods.\n\n  @RequiresNonNull(\"output\")\n  private void maybeLoadInitData() throws IOException, InterruptedException {\n    if (!initDataLoadRequired) {\n      return;\n    }\n    // initDataLoadRequired =>  initDataSource != null && initDataSpec != null\n    Assertions.checkNotNull(initDataSource);\n    Assertions.checkNotNull(initDataSpec);\n    feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted);\n    nextLoadPosition = 0;\n    initDataLoadRequired = false;\n  }\n\n  @RequiresNonNull(\"output\")\n  private void loadMedia() throws IOException, InterruptedException {\n    if (!isMasterTimestampSource) {\n      timestampAdjuster.waitUntilInitialized();\n    } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) {\n      // We're the master and we haven't set the desired first sample timestamp yet.\n      timestampAdjuster.setFirstSampleTimestampUs(startTimeUs);\n    }\n    feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);\n  }\n\n  /**\n   * Attempts to feed the given {@code dataSpec} to {@code this.extractor}. Whenever the operation\n   * concludes (because of a thrown exception or because the operation finishes), the number of fed\n   * bytes is written to {@code nextLoadPosition}.\n   */\n  @RequiresNonNull(\"output\")\n  private void feedDataToExtractor(\n      DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted)\n      throws IOException, InterruptedException {\n    // If we previously fed part of this chunk to the extractor, we need to skip it this time. For\n    // encrypted content we need to skip the data by reading it through the source, so as to ensure\n    // correct decryption of the remainder of the chunk. For clear content, we can request the\n    // remainder of the chunk directly.\n    DataSpec loadDataSpec;\n    boolean skipLoadedBytes;\n    if (dataIsEncrypted) {\n      loadDataSpec = dataSpec;\n      skipLoadedBytes = nextLoadPosition != 0;\n    } else {\n      loadDataSpec = dataSpec.subrange(nextLoadPosition);\n      skipLoadedBytes = false;\n    }\n    try {\n      ExtractorInput input = prepareExtraction(dataSource, loadDataSpec);\n      if (skipLoadedBytes) {\n        input.skipFully(nextLoadPosition);\n      }\n      try {\n        int result = Extractor.RESULT_CONTINUE;\n        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {\n          result = extractor.read(input, DUMMY_POSITION_HOLDER);\n        }\n      } finally {\n        nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n  }\n\n  @RequiresNonNull(\"output\")\n  @EnsuresNonNull(\"extractor\")\n  private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec)\n      throws IOException, InterruptedException {\n    long bytesToRead = dataSource.open(dataSpec);\n\n    DefaultExtractorInput extractorInput =\n        new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead);\n\n    if (extractor == null) {\n      long id3Timestamp = peekId3PrivTimestamp(extractorInput);\n      extractorInput.resetPeekPosition();\n\n      HlsExtractorFactory.Result result =\n          extractorFactory.createExtractor(\n              previousExtractor,\n              dataSpec.uri,\n              trackFormat,\n              muxedCaptionFormats,\n              drmInitData,\n              timestampAdjuster,\n              dataSource.getResponseHeaders(),\n              extractorInput);\n      extractor = result.extractor;\n      isExtractorReusable = result.isReusable;\n      if (result.isPackedAudioExtractor) {\n        output.setSampleOffsetUs(\n            id3Timestamp != C.TIME_UNSET\n                ? timestampAdjuster.adjustTsTimestamp(id3Timestamp)\n                : startTimeUs);\n      } else {\n        // In case the container format changes mid-stream to non-packed-audio, we need to reset\n        // the timestamp offset.\n        output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L);\n      }\n      output.init(uid, shouldSpliceIn, /* reusingExtractor= */ false);\n      extractor.init(output);\n    }\n\n    return extractorInput;\n  }\n\n  /**\n   * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined\n   * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not\n   * found. This method only modifies the peek position.\n   *\n   * @param input The {@link ExtractorInput} to obtain the PRIV frame from.\n   * @return The parsed, adjusted timestamp in microseconds\n   * @throws IOException If an error occurred peeking from the input.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {\n    input.resetPeekPosition();\n    try {\n      input.peekFully(scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);\n    } catch (EOFException e) {\n      // The input isn't long enough for there to be any ID3 data.\n      return C.TIME_UNSET;\n    }\n    scratchId3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);\n    int id = scratchId3Data.readUnsignedInt24();\n    if (id != Id3Decoder.ID3_TAG) {\n      return C.TIME_UNSET;\n    }\n    scratchId3Data.skipBytes(3); // version(2), flags(1).\n    int id3Size = scratchId3Data.readSynchSafeInt();\n    int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;\n    if (requiredCapacity > scratchId3Data.capacity()) {\n      byte[] data = scratchId3Data.data;\n      scratchId3Data.reset(requiredCapacity);\n      System.arraycopy(data, 0, scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);\n    }\n    input.peekFully(scratchId3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size);\n    Metadata metadata = id3Decoder.decode(scratchId3Data.data, id3Size);\n    if (metadata == null) {\n      return C.TIME_UNSET;\n    }\n    int metadataLength = metadata.length();\n    for (int i = 0; i < metadataLength; i++) {\n      Metadata.Entry frame = metadata.get(i);\n      if (frame instanceof PrivFrame) {\n        PrivFrame privFrame = (PrivFrame) frame;\n        if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {\n          System.arraycopy(\n              privFrame.privateData, 0, scratchId3Data.data, 0, 8 /* timestamp size */);\n          scratchId3Data.reset(8);\n          // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the\n          // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495.\n          return scratchId3Data.readLong() & 0x1FFFFFFFFL;\n        }\n      }\n    }\n    return C.TIME_UNSET;\n  }\n\n  // Internal methods.\n\n  private static byte[] getEncryptionIvArray(String ivString) {\n    String trimmedIv;\n    if (Util.toLowerInvariant(ivString).startsWith(\"0x\")) {\n      trimmedIv = ivString.substring(2);\n    } else {\n      trimmedIv = ivString;\n    }\n\n    byte[] ivData = new BigInteger(trimmedIv, /* radix= */ 16).toByteArray();\n    byte[] ivDataWithPadding = new byte[16];\n    int offset = ivData.length > 16 ? ivData.length - 16 : 0;\n    System.arraycopy(\n        ivData,\n        offset,\n        ivDataWithPadding,\n        ivDataWithPadding.length - ivData.length + offset,\n        ivData.length - offset);\n    return ivDataWithPadding;\n  }\n\n  /**\n   * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original\n   * in order to decrypt the loaded data. Else returns the original.\n   *\n   * <p>{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither.\n   */\n  private static DataSource buildDataSource(\n      DataSource dataSource,\n      @Nullable byte[] fullSegmentEncryptionKey,\n      @Nullable byte[] encryptionIv) {\n    if (fullSegmentEncryptionKey != null) {\n      Assertions.checkNotNull(encryptionIv);\n      return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);\n    }\n    return dataSource;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.IdentityHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A {@link MediaPeriod} that loads an HLS stream.\n */\npublic final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,\n    HlsPlaylistTracker.PlaylistEventListener {\n\n  private final HlsExtractorFactory extractorFactory;\n  private final HlsPlaylistTracker playlistTracker;\n  private final HlsDataSourceFactory dataSourceFactory;\n  @Nullable private final TransferListener mediaTransferListener;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final EventDispatcher eventDispatcher;\n  private final Allocator allocator;\n  private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;\n  private final TimestampAdjusterProvider timestampAdjusterProvider;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final boolean allowChunklessPreparation;\n  private final @HlsMediaSource.MetadataType int metadataType;\n  private final boolean useSessionKeys;\n\n  @Nullable private Callback callback;\n  private int pendingPrepareCount;\n  private @MonotonicNonNull TrackGroupArray trackGroups;\n  private HlsSampleStreamWrapper[] sampleStreamWrappers;\n  private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;\n  // Maps sample stream wrappers to variant/rendition index by matching array positions.\n  private int[][] manifestUrlIndicesPerWrapper;\n  private SequenceableLoader compositeSequenceableLoader;\n  private boolean notifiedReadingStarted;\n\n  /**\n   * Creates an HLS media period.\n   *\n   * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments.\n   * @param playlistTracker A tracker for HLS playlists.\n   * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for segments\n   *     and keys.\n   * @param mediaTransferListener The transfer listener to inform of any media data transfers. May\n   *     be null if no listener is available.\n   * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession\n   *     DrmSessions} with.\n   * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n   * @param eventDispatcher A dispatcher to notify of events.\n   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.\n   * @param compositeSequenceableLoaderFactory A factory to create composite {@link\n   *     SequenceableLoader}s for when this media source loads data from multiple streams.\n   * @param allowChunklessPreparation Whether chunkless preparation is allowed.\n   * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.\n   */\n  public HlsMediaPeriod(\n      HlsExtractorFactory extractorFactory,\n      HlsPlaylistTracker playlistTracker,\n      HlsDataSourceFactory dataSourceFactory,\n      @Nullable TransferListener mediaTransferListener,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      Allocator allocator,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      boolean allowChunklessPreparation,\n      @HlsMediaSource.MetadataType int metadataType,\n      boolean useSessionKeys) {\n    this.extractorFactory = extractorFactory;\n    this.playlistTracker = playlistTracker;\n    this.dataSourceFactory = dataSourceFactory;\n    this.mediaTransferListener = mediaTransferListener;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.allocator = allocator;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    this.allowChunklessPreparation = allowChunklessPreparation;\n    this.metadataType = metadataType;\n    this.useSessionKeys = useSessionKeys;\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();\n    streamWrapperIndices = new IdentityHashMap<>();\n    timestampAdjusterProvider = new TimestampAdjusterProvider();\n    sampleStreamWrappers = new HlsSampleStreamWrapper[0];\n    enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];\n    manifestUrlIndicesPerWrapper = new int[0][];\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  public void release() {\n    playlistTracker.removeListener(this);\n    for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {\n      sampleStreamWrapper.release();\n    }\n    callback = null;\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    playlistTracker.addListener(this);\n    buildAndPrepareSampleStreamWrappers(positionUs);\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {\n      sampleStreamWrapper.maybeThrowPrepareError();\n    }\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    // trackGroups will only be null if period hasn't been prepared or has been released.\n    return Assertions.checkNotNull(trackGroups);\n  }\n\n  // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with\n  // null URLs, this method must be updated to calculate stream keys that are compatible with those\n  // that may already be persisted for offline.\n  @Override\n  public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {\n    // See HlsMasterPlaylist.copy for interpretation of StreamKeys.\n    HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());\n    boolean hasVariants = !masterPlaylist.variants.isEmpty();\n    int audioWrapperOffset = hasVariants ? 1 : 0;\n    // Subtitle sample stream wrappers are held last.\n    int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size();\n\n    TrackGroupArray mainWrapperTrackGroups;\n    int mainWrapperPrimaryGroupIndex;\n    int[] mainWrapperVariantIndices;\n    if (hasVariants) {\n      HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0];\n      mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0];\n      mainWrapperTrackGroups = mainWrapper.getTrackGroups();\n      mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex();\n    } else {\n      mainWrapperVariantIndices = new int[0];\n      mainWrapperTrackGroups = TrackGroupArray.EMPTY;\n      mainWrapperPrimaryGroupIndex = 0;\n    }\n\n    List<StreamKey> streamKeys = new ArrayList<>();\n    boolean needsPrimaryTrackGroupSelection = false;\n    boolean hasPrimaryTrackGroupSelection = false;\n    for (TrackSelection trackSelection : trackSelections) {\n      TrackGroup trackSelectionGroup = trackSelection.getTrackGroup();\n      int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup);\n      if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) {\n        if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) {\n          // Primary group in main wrapper.\n          hasPrimaryTrackGroupSelection = true;\n          for (int i = 0; i < trackSelection.length(); i++) {\n            int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)];\n            streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex));\n          }\n        } else {\n          // Embedded group in main wrapper.\n          needsPrimaryTrackGroupSelection = true;\n        }\n      } else {\n        // Audio or subtitle group.\n        for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) {\n          TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups();\n          int selectedTrackGroupIndex = wrapperTrackGroups.indexOf(trackSelectionGroup);\n          if (selectedTrackGroupIndex != C.INDEX_UNSET) {\n            int groupIndexType =\n                i < subtitleWrapperOffset\n                    ? HlsMasterPlaylist.GROUP_INDEX_AUDIO\n                    : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE;\n            int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i];\n            for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) {\n              int renditionIndex =\n                  selectedWrapperUrlIndices[trackSelection.getIndexInTrackGroup(trackIndex)];\n              streamKeys.add(new StreamKey(groupIndexType, renditionIndex));\n            }\n            break;\n          }\n        }\n      }\n    }\n    if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) {\n      // A track selection includes a variant-embedded track, but no variant is added yet. We use\n      // the valid variant with the lowest bitrate to reduce overhead.\n      int lowestBitrateIndex = mainWrapperVariantIndices[0];\n      int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate;\n      for (int i = 1; i < mainWrapperVariantIndices.length; i++) {\n        int variantBitrate =\n            masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate;\n        if (variantBitrate < lowestBitrate) {\n          lowestBitrate = variantBitrate;\n          lowestBitrateIndex = mainWrapperVariantIndices[i];\n        }\n      }\n      streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex));\n    }\n    return streamKeys;\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    // Map each selection and stream onto a child period index.\n    int[] streamChildIndices = new int[selections.length];\n    int[] selectionChildIndices = new int[selections.length];\n    for (int i = 0; i < selections.length; i++) {\n      streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET\n          : streamWrapperIndices.get(streams[i]);\n      selectionChildIndices[i] = C.INDEX_UNSET;\n      if (selections[i] != null) {\n        TrackGroup trackGroup = selections[i].getTrackGroup();\n        for (int j = 0; j < sampleStreamWrappers.length; j++) {\n          if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {\n            selectionChildIndices[i] = j;\n            break;\n          }\n        }\n      }\n    }\n\n    boolean forceReset = false;\n    streamWrapperIndices.clear();\n    // Select tracks for each child, copying the resulting streams back into a new streams array.\n    SampleStream[] newStreams = new SampleStream[selections.length];\n    @NullableType SampleStream[] childStreams = new SampleStream[selections.length];\n    @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];\n    int newEnabledSampleStreamWrapperCount = 0;\n    HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers =\n        new HlsSampleStreamWrapper[sampleStreamWrappers.length];\n    for (int i = 0; i < sampleStreamWrappers.length; i++) {\n      for (int j = 0; j < selections.length; j++) {\n        childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;\n        childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;\n      }\n      HlsSampleStreamWrapper sampleStreamWrapper = sampleStreamWrappers[i];\n      boolean wasReset = sampleStreamWrapper.selectTracks(childSelections, mayRetainStreamFlags,\n          childStreams, streamResetFlags, positionUs, forceReset);\n      boolean wrapperEnabled = false;\n      for (int j = 0; j < selections.length; j++) {\n        SampleStream childStream = childStreams[j];\n        if (selectionChildIndices[j] == i) {\n          // Assert that the child provided a stream for the selection.\n          Assertions.checkNotNull(childStream);\n          newStreams[j] = childStream;\n          wrapperEnabled = true;\n          streamWrapperIndices.put(childStream, i);\n        } else if (streamChildIndices[j] == i) {\n          // Assert that the child cleared any previous stream.\n          Assertions.checkState(childStream == null);\n        }\n      }\n      if (wrapperEnabled) {\n        newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;\n        if (newEnabledSampleStreamWrapperCount++ == 0) {\n          // The first enabled wrapper is responsible for initializing timestamp adjusters. This\n          // way, if enabled, variants are responsible. Else audio renditions. Else text renditions.\n          sampleStreamWrapper.setIsTimestampMaster(true);\n          if (wasReset || enabledSampleStreamWrappers.length == 0\n              || sampleStreamWrapper != enabledSampleStreamWrappers[0]) {\n            // The wrapper responsible for initializing the timestamp adjusters was reset or\n            // changed. We need to reset the timestamp adjuster provider and all other wrappers.\n            timestampAdjusterProvider.reset();\n            forceReset = true;\n          }\n        } else {\n          sampleStreamWrapper.setIsTimestampMaster(false);\n        }\n      }\n    }\n    // Copy the new streams back into the streams array.\n    System.arraycopy(newStreams, 0, streams, 0, newStreams.length);\n    // Update the local state.\n    enabledSampleStreamWrappers =\n        Util.nullSafeArrayCopy(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount);\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(\n            enabledSampleStreamWrappers);\n    return positionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {\n      sampleStreamWrapper.discardBuffer(positionUs, toKeyframe);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    compositeSequenceableLoader.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    if (trackGroups == null) {\n      // Preparation is still going on.\n      for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) {\n        wrapper.continuePreparing();\n      }\n      return false;\n    } else {\n      return compositeSequenceableLoader.continueLoading(positionUs);\n    }\n  }\n\n  @Override\n  public boolean isLoading() {\n    return compositeSequenceableLoader.isLoading();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return compositeSequenceableLoader.getNextLoadPositionUs();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    return C.TIME_UNSET;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return compositeSequenceableLoader.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    if (enabledSampleStreamWrappers.length > 0) {\n      // We need to reset all wrappers if the one responsible for initializing timestamp adjusters\n      // is reset. Else each wrapper can decide whether to reset independently.\n      boolean forceReset = enabledSampleStreamWrappers[0].seekToUs(positionUs, false);\n      for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {\n        enabledSampleStreamWrappers[i].seekToUs(positionUs, forceReset);\n      }\n      if (forceReset) {\n        timestampAdjusterProvider.reset();\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return positionUs;\n  }\n\n  // HlsSampleStreamWrapper.Callback implementation.\n\n  @Override\n  public void onPrepared() {\n    if (--pendingPrepareCount > 0) {\n      return;\n    }\n\n    int totalTrackGroupCount = 0;\n    for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {\n      totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;\n    }\n    TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];\n    int trackGroupIndex = 0;\n    for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {\n      int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length;\n      for (int j = 0; j < wrapperTrackGroupCount; j++) {\n        trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j);\n      }\n    }\n    trackGroups = new TrackGroupArray(trackGroupArray);\n    callback.onPrepared(this);\n  }\n\n  @Override\n  public void onPlaylistRefreshRequired(Uri url) {\n    playlistTracker.refreshPlaylist(url);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  // PlaylistListener implementation.\n\n  @Override\n  public void onPlaylistChanged() {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  @Override\n  public boolean onPlaylistError(Uri url, long blacklistDurationMs) {\n    boolean noBlacklistingFailure = true;\n    for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {\n      noBlacklistingFailure &= streamWrapper.onPlaylistError(url, blacklistDurationMs);\n    }\n    callback.onContinueLoadingRequested(this);\n    return noBlacklistingFailure;\n  }\n\n  // Internal methods.\n\n  private void buildAndPrepareSampleStreamWrappers(long positionUs) {\n    HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());\n    Map<String, DrmInitData> overridingDrmInitData =\n        useSessionKeys\n            ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData)\n            : Collections.emptyMap();\n\n    boolean hasVariants = !masterPlaylist.variants.isEmpty();\n    List<Rendition> audioRenditions = masterPlaylist.audios;\n    List<Rendition> subtitleRenditions = masterPlaylist.subtitles;\n\n    pendingPrepareCount = 0;\n    ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();\n    ArrayList<int[]> manifestUrlIndicesPerWrapper = new ArrayList<>();\n\n    if (hasVariants) {\n      buildAndPrepareMainSampleStreamWrapper(\n          masterPlaylist,\n          positionUs,\n          sampleStreamWrappers,\n          manifestUrlIndicesPerWrapper,\n          overridingDrmInitData);\n    }\n\n    // TODO: Build video stream wrappers here.\n\n    buildAndPrepareAudioSampleStreamWrappers(\n        positionUs,\n        audioRenditions,\n        sampleStreamWrappers,\n        manifestUrlIndicesPerWrapper,\n        overridingDrmInitData);\n\n    // Subtitle stream wrappers. We can always use master playlist information to prepare these.\n    for (int i = 0; i < subtitleRenditions.size(); i++) {\n      Rendition subtitleRendition = subtitleRenditions.get(i);\n      HlsSampleStreamWrapper sampleStreamWrapper =\n          buildSampleStreamWrapper(\n              C.TRACK_TYPE_TEXT,\n              new Uri[] {subtitleRendition.url},\n              new Format[] {subtitleRendition.format},\n              null,\n              Collections.emptyList(),\n              overridingDrmInitData,\n              positionUs);\n      manifestUrlIndicesPerWrapper.add(new int[] {i});\n      sampleStreamWrappers.add(sampleStreamWrapper);\n      sampleStreamWrapper.prepareWithMasterPlaylistInfo(\n          new TrackGroup[] {new TrackGroup(subtitleRendition.format)},\n          /* primaryTrackGroupIndex= */ 0);\n    }\n\n    this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]);\n    this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]);\n    pendingPrepareCount = this.sampleStreamWrappers.length;\n    // Set timestamp master and trigger preparation (if not already prepared)\n    this.sampleStreamWrappers[0].setIsTimestampMaster(true);\n    for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) {\n      sampleStreamWrapper.continuePreparing();\n    }\n    // All wrappers are enabled during preparation.\n    enabledSampleStreamWrappers = this.sampleStreamWrappers;\n  }\n\n  /**\n   * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}.\n   *\n   * <p>The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It\n   * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive\n   * and may contain multiple muxed tracks.\n   *\n   * <p>If chunkless preparation is allowed, the media period will try preparation without segment\n   * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional\n   * preparation with segment downloads will take place. The following points apply to chunkless\n   * preparation:\n   *\n   * <ul>\n   *   <li>A muxed audio track will be exposed if the codecs list contain an audio entry and the\n   *       master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not\n   *       contain any EXT-X-MEDIA tag.\n   *   <li>Closed captions will only be exposed if they are declared by the master playlist.\n   *   <li>An ID3 track is exposed preemptively, in case the segments contain an ID3 track.\n   * </ul>\n   *\n   * @param masterPlaylist The HLS master playlist.\n   * @param positionUs If preparation requires any chunk downloads, the position in microseconds at\n   *     which downloading should start. Ignored otherwise.\n   * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added.\n   * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added.\n   * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type\n   *     (i.e. {@link DrmInitData#schemeType}).\n   */\n  private void buildAndPrepareMainSampleStreamWrapper(\n      HlsMasterPlaylist masterPlaylist,\n      long positionUs,\n      List<HlsSampleStreamWrapper> sampleStreamWrappers,\n      List<int[]> manifestUrlIndicesPerWrapper,\n      Map<String, DrmInitData> overridingDrmInitData) {\n    int[] variantTypes = new int[masterPlaylist.variants.size()];\n    int videoVariantCount = 0;\n    int audioVariantCount = 0;\n    for (int i = 0; i < masterPlaylist.variants.size(); i++) {\n      Variant variant = masterPlaylist.variants.get(i);\n      Format format = variant.format;\n      if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) {\n        variantTypes[i] = C.TRACK_TYPE_VIDEO;\n        videoVariantCount++;\n      } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) {\n        variantTypes[i] = C.TRACK_TYPE_AUDIO;\n        audioVariantCount++;\n      } else {\n        variantTypes[i] = C.TRACK_TYPE_UNKNOWN;\n      }\n    }\n    boolean useVideoVariantsOnly = false;\n    boolean useNonAudioVariantsOnly = false;\n    int selectedVariantsCount = variantTypes.length;\n    if (videoVariantCount > 0) {\n      // We've identified some variants as definitely containing video. Assume variants within the\n      // master playlist are marked consistently, and hence that we have the full set. Filter out\n      // any other variants, which are likely to be audio only.\n      useVideoVariantsOnly = true;\n      selectedVariantsCount = videoVariantCount;\n    } else if (audioVariantCount < variantTypes.length) {\n      // We've identified some variants, but not all, as being audio only. Filter them out to leave\n      // the remaining variants, which are likely to contain video.\n      useNonAudioVariantsOnly = true;\n      selectedVariantsCount = variantTypes.length - audioVariantCount;\n    }\n    Uri[] selectedPlaylistUrls = new Uri[selectedVariantsCount];\n    Format[] selectedPlaylistFormats = new Format[selectedVariantsCount];\n    int[] selectedVariantIndices = new int[selectedVariantsCount];\n    int outIndex = 0;\n    for (int i = 0; i < masterPlaylist.variants.size(); i++) {\n      if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO)\n          && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) {\n        Variant variant = masterPlaylist.variants.get(i);\n        selectedPlaylistUrls[outIndex] = variant.url;\n        selectedPlaylistFormats[outIndex] = variant.format;\n        selectedVariantIndices[outIndex++] = i;\n      }\n    }\n    String codecs = selectedPlaylistFormats[0].codecs;\n    HlsSampleStreamWrapper sampleStreamWrapper =\n        buildSampleStreamWrapper(\n            C.TRACK_TYPE_DEFAULT,\n            selectedPlaylistUrls,\n            selectedPlaylistFormats,\n            masterPlaylist.muxedAudioFormat,\n            masterPlaylist.muxedCaptionFormats,\n            overridingDrmInitData,\n            positionUs);\n    sampleStreamWrappers.add(sampleStreamWrapper);\n    manifestUrlIndicesPerWrapper.add(selectedVariantIndices);\n    if (allowChunklessPreparation && codecs != null) {\n      boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;\n      boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;\n      List<TrackGroup> muxedTrackGroups = new ArrayList<>();\n      if (variantsContainVideoCodecs) {\n        Format[] videoFormats = new Format[selectedVariantsCount];\n        for (int i = 0; i < videoFormats.length; i++) {\n          videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);\n        }\n        muxedTrackGroups.add(new TrackGroup(videoFormats));\n\n        if (variantsContainAudioCodecs\n            && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {\n          muxedTrackGroups.add(\n              new TrackGroup(\n                  deriveAudioFormat(\n                      selectedPlaylistFormats[0],\n                      masterPlaylist.muxedAudioFormat,\n                      /* isPrimaryTrackInVariant= */ false)));\n        }\n        List<Format> ccFormats = masterPlaylist.muxedCaptionFormats;\n        if (ccFormats != null) {\n          for (int i = 0; i < ccFormats.size(); i++) {\n            muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));\n          }\n        }\n      } else if (variantsContainAudioCodecs) {\n        // Variants only contain audio.\n        Format[] audioFormats = new Format[selectedVariantsCount];\n        for (int i = 0; i < audioFormats.length; i++) {\n          audioFormats[i] =\n              deriveAudioFormat(\n                  /* variantFormat= */ selectedPlaylistFormats[i],\n                  masterPlaylist.muxedAudioFormat,\n                  /* isPrimaryTrackInVariant= */ true);\n        }\n        muxedTrackGroups.add(new TrackGroup(audioFormats));\n      } else {\n        // Variants contain codecs but no video or audio entries could be identified.\n        throw new IllegalArgumentException(\"Unexpected codecs attribute: \" + codecs);\n      }\n\n      TrackGroup id3TrackGroup =\n          new TrackGroup(\n              Format.createSampleFormat(\n                  /* id= */ \"ID3\",\n                  MimeTypes.APPLICATION_ID3,\n                  /* codecs= */ null,\n                  /* bitrate= */ Format.NO_VALUE,\n                  /* drmInitData= */ null));\n      muxedTrackGroups.add(id3TrackGroup);\n\n      sampleStreamWrapper.prepareWithMasterPlaylistInfo(\n          muxedTrackGroups.toArray(new TrackGroup[0]),\n          /* primaryTrackGroupIndex= */ 0,\n          /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup));\n    }\n  }\n\n  private void buildAndPrepareAudioSampleStreamWrappers(\n      long positionUs,\n      List<Rendition> audioRenditions,\n      List<HlsSampleStreamWrapper> sampleStreamWrappers,\n      List<int[]> manifestUrlsIndicesPerWrapper,\n      Map<String, DrmInitData> overridingDrmInitData) {\n    ArrayList<Uri> scratchPlaylistUrls =\n        new ArrayList<>(/* initialCapacity= */ audioRenditions.size());\n    ArrayList<Format> scratchPlaylistFormats =\n        new ArrayList<>(/* initialCapacity= */ audioRenditions.size());\n    ArrayList<Integer> scratchIndicesList =\n        new ArrayList<>(/* initialCapacity= */ audioRenditions.size());\n    HashSet<String> alreadyGroupedNames = new HashSet<>();\n    for (int renditionByNameIndex = 0;\n        renditionByNameIndex < audioRenditions.size();\n        renditionByNameIndex++) {\n      String name = audioRenditions.get(renditionByNameIndex).name;\n      if (!alreadyGroupedNames.add(name)) {\n        // This name already has a corresponding group.\n        continue;\n      }\n\n      boolean renditionsHaveCodecs = true;\n      scratchPlaylistUrls.clear();\n      scratchPlaylistFormats.clear();\n      scratchIndicesList.clear();\n      // Group all renditions with matching name.\n      for (int renditionIndex = 0; renditionIndex < audioRenditions.size(); renditionIndex++) {\n        if (Util.areEqual(name, audioRenditions.get(renditionIndex).name)) {\n          Rendition rendition = audioRenditions.get(renditionIndex);\n          scratchIndicesList.add(renditionIndex);\n          scratchPlaylistUrls.add(rendition.url);\n          scratchPlaylistFormats.add(rendition.format);\n          renditionsHaveCodecs &= rendition.format.codecs != null;\n        }\n      }\n\n      HlsSampleStreamWrapper sampleStreamWrapper =\n          buildSampleStreamWrapper(\n              C.TRACK_TYPE_AUDIO,\n              scratchPlaylistUrls.toArray(Util.castNonNullTypeArray(new Uri[0])),\n              scratchPlaylistFormats.toArray(new Format[0]),\n              /* muxedAudioFormat= */ null,\n              /* muxedCaptionFormats= */ Collections.emptyList(),\n              overridingDrmInitData,\n              positionUs);\n      manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList));\n      sampleStreamWrappers.add(sampleStreamWrapper);\n\n      if (allowChunklessPreparation && renditionsHaveCodecs) {\n        Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);\n        sampleStreamWrapper.prepareWithMasterPlaylistInfo(\n            new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);\n      }\n    }\n  }\n\n  private HlsSampleStreamWrapper buildSampleStreamWrapper(\n      int trackType,\n      Uri[] playlistUrls,\n      Format[] playlistFormats,\n      @Nullable Format muxedAudioFormat,\n      @Nullable List<Format> muxedCaptionFormats,\n      Map<String, DrmInitData> overridingDrmInitData,\n      long positionUs) {\n    HlsChunkSource defaultChunkSource =\n        new HlsChunkSource(\n            extractorFactory,\n            playlistTracker,\n            playlistUrls,\n            playlistFormats,\n            dataSourceFactory,\n            mediaTransferListener,\n            timestampAdjusterProvider,\n            muxedCaptionFormats);\n    return new HlsSampleStreamWrapper(\n        trackType,\n        /* callback= */ this,\n        defaultChunkSource,\n        overridingDrmInitData,\n        allocator,\n        positionUs,\n        muxedAudioFormat,\n        drmSessionManager,\n        loadErrorHandlingPolicy,\n        eventDispatcher,\n        metadataType);\n  }\n\n  private static Map<String, DrmInitData> deriveOverridingDrmInitData(\n      List<DrmInitData> sessionKeyDrmInitData) {\n    ArrayList<DrmInitData> mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData);\n    HashMap<String, DrmInitData> drmInitDataBySchemeType = new HashMap<>();\n    for (int i = 0; i < mutableSessionKeyDrmInitData.size(); i++) {\n      DrmInitData drmInitData = sessionKeyDrmInitData.get(i);\n      String scheme = drmInitData.schemeType;\n      // Merge any subsequent drmInitData instances that have the same scheme type. This is valid\n      // due to the assumptions documented on HlsMediaSource.Builder.setUseSessionKeys, and is\n      // necessary to get data for different CDNs (e.g. Widevine and PlayReady) into a single\n      // drmInitData.\n      int j = i + 1;\n      while (j < mutableSessionKeyDrmInitData.size()) {\n        DrmInitData nextDrmInitData = mutableSessionKeyDrmInitData.get(j);\n        if (TextUtils.equals(nextDrmInitData.schemeType, scheme)) {\n          drmInitData = drmInitData.merge(nextDrmInitData);\n          mutableSessionKeyDrmInitData.remove(j);\n        } else {\n          j++;\n        }\n      }\n      drmInitDataBySchemeType.put(scheme, drmInitData);\n    }\n    return drmInitDataBySchemeType;\n  }\n\n  private static Format deriveVideoFormat(Format variantFormat) {\n    String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);\n    String sampleMimeType = MimeTypes.getMediaMimeType(codecs);\n    return Format.createVideoContainerFormat(\n        variantFormat.id,\n        variantFormat.label,\n        variantFormat.containerMimeType,\n        sampleMimeType,\n        codecs,\n        variantFormat.metadata,\n        variantFormat.bitrate,\n        variantFormat.width,\n        variantFormat.height,\n        variantFormat.frameRate,\n        /* initializationData= */ null,\n        variantFormat.selectionFlags,\n        variantFormat.roleFlags);\n  }\n\n  private static Format deriveAudioFormat(\n      Format variantFormat, @Nullable Format mediaTagFormat, boolean isPrimaryTrackInVariant) {\n    String codecs;\n    Metadata metadata;\n    int channelCount = Format.NO_VALUE;\n    int selectionFlags = 0;\n    int roleFlags = 0;\n    String language = null;\n    String label = null;\n    if (mediaTagFormat != null) {\n      codecs = mediaTagFormat.codecs;\n      metadata = mediaTagFormat.metadata;\n      channelCount = mediaTagFormat.channelCount;\n      selectionFlags = mediaTagFormat.selectionFlags;\n      roleFlags = mediaTagFormat.roleFlags;\n      language = mediaTagFormat.language;\n      label = mediaTagFormat.label;\n    } else {\n      codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO);\n      metadata = variantFormat.metadata;\n      if (isPrimaryTrackInVariant) {\n        channelCount = variantFormat.channelCount;\n        selectionFlags = variantFormat.selectionFlags;\n        roleFlags = variantFormat.roleFlags;\n        language = variantFormat.language;\n        label = variantFormat.label;\n      }\n    }\n    String sampleMimeType = MimeTypes.getMediaMimeType(codecs);\n    int bitrate = isPrimaryTrackInVariant ? variantFormat.bitrate : Format.NO_VALUE;\n    return Format.createAudioContainerFormat(\n        variantFormat.id,\n        label,\n        variantFormat.containerMimeType,\n        sampleMimeType,\n        codecs,\n        metadata,\n        bitrate,\n        channelCount,\n        /* sampleRate= */ Format.NO_VALUE,\n        /* initializationData= */ null,\n        selectionFlags,\n        roleFlags,\n        language);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport static java.lang.annotation.RetentionPolicy.SOURCE;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.BaseMediaSource;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.MediaSourceFactory;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;\nimport com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;\nimport com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.util.List;\n\n/** An HLS {@link MediaSource}. */\npublic final class HlsMediaSource extends BaseMediaSource\n    implements HlsPlaylistTracker.PrimaryPlaylistListener {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.hls\");\n  }\n\n  /**\n   * The types of metadata that can be extracted from HLS streams.\n   *\n   * <p>Allowed values:\n   *\n   * <ul>\n   *   <li>{@link #METADATA_TYPE_ID3}\n   *   <li>{@link #METADATA_TYPE_EMSG}\n   * </ul>\n   *\n   * <p>See {@link Factory#setMetadataType(int)}.\n   */\n  @Documented\n  @Retention(SOURCE)\n  @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG})\n  public @interface MetadataType {}\n\n  /** Type for ID3 metadata in HLS streams. */\n  public static final int METADATA_TYPE_ID3 = 1;\n  /** Type for ESMG metadata in HLS streams. */\n  public static final int METADATA_TYPE_EMSG = 3;\n\n  /** Factory for {@link HlsMediaSource}s. */\n  public static final class Factory implements MediaSourceFactory {\n\n    private final HlsDataSourceFactory hlsDataSourceFactory;\n\n    private HlsExtractorFactory extractorFactory;\n    private HlsPlaylistParserFactory playlistParserFactory;\n    @Nullable private List<StreamKey> streamKeys;\n    private HlsPlaylistTracker.Factory playlistTrackerFactory;\n    private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n    private DrmSessionManager<?> drmSessionManager;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private boolean allowChunklessPreparation;\n    @MetadataType private int metadataType;\n    private boolean useSessionKeys;\n    private boolean isCreateCalled;\n    @Nullable private Object tag;\n\n    /**\n     * Creates a new factory for {@link HlsMediaSource}s.\n     *\n     * @param dataSourceFactory A data source factory that will be wrapped by a {@link\n     *     DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and\n     *     keys.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this(new DefaultHlsDataSourceFactory(dataSourceFactory));\n    }\n\n    /**\n     * Creates a new factory for {@link HlsMediaSource}s.\n     *\n     * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for\n     *     manifests, segments and keys.\n     */\n    public Factory(HlsDataSourceFactory hlsDataSourceFactory) {\n      this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory);\n      playlistParserFactory = new DefaultHlsPlaylistParserFactory();\n      playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;\n      extractorFactory = HlsExtractorFactory.DEFAULT;\n      drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();\n      metadataType = METADATA_TYPE_ID3;\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link\n     * com.google.android.exoplayer2.Timeline} of the source as {@link\n     * com.google.android.exoplayer2.Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTag(@Nullable Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the factory for {@link Extractor}s for the segments. The default value is {@link\n     * HlsExtractorFactory#DEFAULT}.\n     *\n     * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the\n     *     segments.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.extractorFactory = Assertions.checkNotNull(extractorFactory);\n      return this;\n    }\n\n    /**\n     * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The\n     * default value is {@link DrmSessionManager#DUMMY}.\n     *\n     * @param drmSessionManager The {@link DrmSessionManager}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {\n      Assertions.checkState(!isCreateCalled);\n      this.drmSessionManager = drmSessionManager;\n      return this;\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /**\n     * Sets the minimum number of times to retry if a loading error occurs. The default value is\n     * {@link DefaultLoadErrorHandlingPolicy#DEFAULT_MIN_LOADABLE_RETRY_COUNT}.\n     *\n     * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\n     * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\n     * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\n     *\n     * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\n     */\n    @Deprecated\n    public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount);\n      return this;\n    }\n\n    /**\n     * Sets the factory from which playlist parsers will be obtained. The default value is a {@link\n     * DefaultHlsPlaylistParserFactory}.\n     *\n     * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.playlistParserFactory = Assertions.checkNotNull(playlistParserFactory);\n      return this;\n    }\n\n    /**\n     * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link\n     * DefaultHlsPlaylistTracker#FACTORY}.\n     *\n     * @param playlistTrackerFactory A factory for {@link HlsPlaylistTracker} instances.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setPlaylistTrackerFactory(HlsPlaylistTracker.Factory playlistTrackerFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.playlistTrackerFactory = Assertions.checkNotNull(playlistTrackerFactory);\n      return this;\n    }\n\n    /**\n     * Sets the factory to create composite {@link SequenceableLoader}s for when this media source\n     * loads data from multiple streams (video, audio etc...). The default is an instance of {@link\n     * DefaultCompositeSequenceableLoaderFactory}.\n     *\n     * @param compositeSequenceableLoaderFactory A factory to create composite {@link\n     *     SequenceableLoader}s for when this media source loads data from multiple streams (video,\n     *     audio etc...).\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setCompositeSequenceableLoaderFactory(\n        CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.compositeSequenceableLoaderFactory =\n          Assertions.checkNotNull(compositeSequenceableLoaderFactory);\n      return this;\n    }\n\n    /**\n     * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads\n     * will be enabled for streams that provide sufficient information in their master playlist.\n     *\n     * @param allowChunklessPreparation Whether chunkless preparation is allowed.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) {\n      Assertions.checkState(!isCreateCalled);\n      this.allowChunklessPreparation = allowChunklessPreparation;\n      return this;\n    }\n\n    /**\n     * Sets the type of metadata to extract from the HLS source (defaults to {@link\n     * #METADATA_TYPE_ID3}).\n     *\n     * <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is\n     * wrapped in an EMSG box [<a href=\"https://aomediacodec.github.io/av1-id3/\">spec</a>].\n     *\n     * <p>If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted\n     * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant\n     * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be\n     * dropped.\n     *\n     * <p>If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant\n     * stream will be extracted. No metadata will be extracted from TS streams, since they don't\n     * support EMSG.\n     *\n     * @param metadataType The type of metadata to extract.\n     * @return This factory, for convenience.\n     */\n    public Factory setMetadataType(@MetadataType int metadataType) {\n      Assertions.checkState(!isCreateCalled);\n      this.metadataType = metadataType;\n      return this;\n    }\n\n    /**\n     * Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's\n     * assumed that any single session key declared in the master playlist can be used to obtain all\n     * of the keys required for playback. For media where this is not true, this option should not\n     * be enabled.\n     *\n     * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.\n     * @return This factory, for convenience.\n     */\n    public Factory setUseSessionKeys(boolean useSessionKeys) {\n      this.useSessionKeys = useSessionKeys;\n      return this;\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,\n     *     MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public HlsMediaSource createMediaSource(\n        Uri playlistUri,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      HlsMediaSource mediaSource = createMediaSource(playlistUri);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    /**\n     * Returns a new {@link HlsMediaSource} using the current parameters.\n     *\n     * @return The new {@link HlsMediaSource}.\n     */\n    @Override\n    public HlsMediaSource createMediaSource(Uri playlistUri) {\n      isCreateCalled = true;\n      if (streamKeys != null) {\n        playlistParserFactory =\n            new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);\n      }\n      return new HlsMediaSource(\n          playlistUri,\n          hlsDataSourceFactory,\n          extractorFactory,\n          compositeSequenceableLoaderFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          playlistTrackerFactory.createTracker(\n              hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),\n          allowChunklessPreparation,\n          metadataType,\n          useSessionKeys,\n          tag);\n    }\n\n    @Override\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\n      return this;\n    }\n\n    @Override\n    public int[] getSupportedTypes() {\n      return new int[] {C.TYPE_HLS};\n    }\n\n  }\n\n  private final HlsExtractorFactory extractorFactory;\n  private final Uri manifestUri;\n  private final HlsDataSourceFactory dataSourceFactory;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final boolean allowChunklessPreparation;\n  private final @MetadataType int metadataType;\n  private final boolean useSessionKeys;\n  private final HlsPlaylistTracker playlistTracker;\n  @Nullable private final Object tag;\n\n  @Nullable private TransferListener mediaTransferListener;\n\n  private HlsMediaSource(\n      Uri manifestUri,\n      HlsDataSourceFactory dataSourceFactory,\n      HlsExtractorFactory extractorFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      HlsPlaylistTracker playlistTracker,\n      boolean allowChunklessPreparation,\n      @MetadataType int metadataType,\n      boolean useSessionKeys,\n      @Nullable Object tag) {\n    this.manifestUri = manifestUri;\n    this.dataSourceFactory = dataSourceFactory;\n    this.extractorFactory = extractorFactory;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.playlistTracker = playlistTracker;\n    this.allowChunklessPreparation = allowChunklessPreparation;\n    this.metadataType = metadataType;\n    this.useSessionKeys = useSessionKeys;\n    this.tag = tag;\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\n    drmSessionManager.prepare();\n    EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);\n    playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    playlistTracker.maybeThrowPrimaryPlaylistRefreshError();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    EventDispatcher eventDispatcher = createEventDispatcher(id);\n    return new HlsMediaPeriod(\n        extractorFactory,\n        playlistTracker,\n        dataSourceFactory,\n        mediaTransferListener,\n        drmSessionManager,\n        loadErrorHandlingPolicy,\n        eventDispatcher,\n        allocator,\n        compositeSequenceableLoaderFactory,\n        allowChunklessPreparation,\n        metadataType,\n        useSessionKeys);\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    ((HlsMediaPeriod) mediaPeriod).release();\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    playlistTracker.stop();\n    drmSessionManager.release();\n  }\n\n  @Override\n  public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {\n    SinglePeriodTimeline timeline;\n    long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs)\n        : C.TIME_UNSET;\n    // For playlist types EVENT and VOD we know segments are never removed, so the presentation\n    // started at the same time as the window. Otherwise, we don't know the presentation start time.\n    long presentationStartTimeMs =\n        playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT\n                || playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD\n            ? windowStartTimeMs\n            : C.TIME_UNSET;\n    long windowDefaultStartPositionUs = playlist.startOffsetUs;\n    // masterPlaylist is non-null because the first playlist has been fetched by now.\n    HlsManifest manifest =\n        new HlsManifest(Assertions.checkNotNull(playlistTracker.getMasterPlaylist()), playlist);\n    if (playlistTracker.isLive()) {\n      long offsetFromInitialStartTimeUs =\n          playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();\n      long periodDurationUs =\n          playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;\n      List<HlsMediaPlaylist.Segment> segments = playlist.segments;\n      if (windowDefaultStartPositionUs == C.TIME_UNSET) {\n        windowDefaultStartPositionUs = 0;\n        if (!segments.isEmpty()) {\n          int defaultStartSegmentIndex = Math.max(0, segments.size() - 3);\n          // We attempt to set the default start position to be at least twice the target duration\n          // behind the live edge.\n          long minStartPositionUs = playlist.durationUs - playlist.targetDurationUs * 2;\n          while (defaultStartSegmentIndex > 0\n              && segments.get(defaultStartSegmentIndex).relativeStartTimeUs > minStartPositionUs) {\n            defaultStartSegmentIndex--;\n          }\n          windowDefaultStartPositionUs = segments.get(defaultStartSegmentIndex).relativeStartTimeUs;\n        }\n      }\n      timeline =\n          new SinglePeriodTimeline(\n              presentationStartTimeMs,\n              windowStartTimeMs,\n              periodDurationUs,\n              /* windowDurationUs= */ playlist.durationUs,\n              /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,\n              windowDefaultStartPositionUs,\n              /* isSeekable= */ true,\n              /* isDynamic= */ !playlist.hasEndTag,\n              /* isLive= */ true,\n              manifest,\n              tag);\n    } else /* not live */ {\n      if (windowDefaultStartPositionUs == C.TIME_UNSET) {\n        windowDefaultStartPositionUs = 0;\n      }\n      timeline =\n          new SinglePeriodTimeline(\n              presentationStartTimeMs,\n              windowStartTimeMs,\n              /* periodDurationUs= */ playlist.durationUs,\n              /* windowDurationUs= */ playlist.durationUs,\n              /* windowPositionInPeriodUs= */ 0,\n              windowDefaultStartPositionUs,\n              /* isSeekable= */ true,\n              /* isDynamic= */ false,\n              /* isLive= */ false,\n              manifest,\n              tag);\n    }\n    refreshSourceInfo(timeline);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\n\n/**\n * {@link SampleStream} for a particular sample queue in HLS.\n */\n/* package */ final class HlsSampleStream implements SampleStream {\n\n  private final int trackGroupIndex;\n  private final HlsSampleStreamWrapper sampleStreamWrapper;\n  private int sampleQueueIndex;\n\n  public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) {\n    this.sampleStreamWrapper = sampleStreamWrapper;\n    this.trackGroupIndex = trackGroupIndex;\n    sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;\n  }\n\n  public void bindSampleQueue() {\n    Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);\n    sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);\n  }\n\n  public void unbindSampleQueue() {\n    if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {\n      sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);\n      sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;\n    }\n  }\n\n  // SampleStream implementation.\n\n  @Override\n  public boolean isReady() {\n    return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL\n        || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {\n      throw new SampleQueueMappingException(\n          sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);\n    } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {\n      sampleStreamWrapper.maybeThrowError();\n    } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {\n      sampleStreamWrapper.maybeThrowError(sampleQueueIndex);\n    }\n  }\n\n  @Override\n  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {\n    if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {\n      buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n      return C.RESULT_BUFFER_READ;\n    }\n    return hasValidSampleQueueIndex()\n        ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)\n        : C.RESULT_NOTHING_READ;\n  }\n\n  @Override\n  public int skipData(long positionUs) {\n    return hasValidSampleQueueIndex()\n        ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)\n        : 0;\n  }\n\n  // Internal methods.\n\n  private boolean hasValidSampleQueueIndex() {\n    return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING\n        && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL\n        && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.util.SparseIntArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.extractor.DummyTrackOutput;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;\nimport com.google.android.exoplayer2.metadata.id3.PrivFrame;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleQueue;\nimport com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides\n * {@link SampleStream}s from which the loaded media can be consumed.\n */\n/* package */ final class HlsSampleStreamWrapper implements Loader.Callback<Chunk>,\n    Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {\n\n  /**\n   * A callback to be notified of events.\n   */\n  public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWrapper> {\n\n    /**\n     * Called when the wrapper has been prepared.\n     *\n     * <p>Note: This method will be called on a later handler loop than the one on which either\n     * {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked.\n     */\n    void onPrepared();\n\n    /**\n     * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the\n     * given url changes.\n     */\n    void onPlaylistRefreshRequired(Uri playlistUrl);\n  }\n\n  private static final String TAG = \"HlsSampleStreamWrapper\";\n\n  public static final int SAMPLE_QUEUE_INDEX_PENDING = -1;\n  public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2;\n  public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3;\n\n  private static final Set<Integer> MAPPABLE_TYPES =\n      Collections.unmodifiableSet(\n          new HashSet<>(\n              Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA)));\n\n  private final int trackType;\n  private final Callback callback;\n  private final HlsChunkSource chunkSource;\n  private final Allocator allocator;\n  @Nullable private final Format muxedAudioFormat;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final Loader loader;\n  private final EventDispatcher eventDispatcher;\n  private final @HlsMediaSource.MetadataType int metadataType;\n  private final HlsChunkSource.HlsChunkHolder nextChunkHolder;\n  private final ArrayList<HlsMediaChunk> mediaChunks;\n  private final List<HlsMediaChunk> readOnlyMediaChunks;\n  // Using runnables rather than in-line method references to avoid repeated allocations.\n  private final Runnable maybeFinishPrepareRunnable;\n  private final Runnable onTracksEndedRunnable;\n  private final Handler handler;\n  private final ArrayList<HlsSampleStream> hlsSampleStreams;\n  private final Map<String, DrmInitData> overridingDrmInitData;\n\n  private SampleQueue[] sampleQueues;\n  private int[] sampleQueueTrackIds;\n  private Set<Integer> sampleQueueMappingDoneByType;\n  private SparseIntArray sampleQueueIndicesByType;\n  @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput;\n  private int primarySampleQueueType;\n  private int primarySampleQueueIndex;\n  private boolean sampleQueuesBuilt;\n  private boolean prepared;\n  private int enabledTrackGroupCount;\n  @MonotonicNonNull private Format upstreamTrackFormat;\n  @Nullable private Format downstreamTrackFormat;\n  private boolean released;\n\n  // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details.\n  // Indexed by track (as exposed by this source).\n  @MonotonicNonNull private TrackGroupArray trackGroups;\n  @MonotonicNonNull private Set<TrackGroup> optionalTrackGroups;\n  // Indexed by track group.\n  private int @MonotonicNonNull [] trackGroupToSampleQueueIndex;\n  private int primaryTrackGroupIndex;\n  private boolean haveAudioVideoSampleQueues;\n  private boolean[] sampleQueuesEnabledStates;\n  private boolean[] sampleQueueIsAudioVideoFlags;\n\n  private long lastSeekPositionUs;\n  private long pendingResetPositionUs;\n  private boolean pendingResetUpstreamFormats;\n  private boolean seenFirstTrackSelection;\n  private boolean loadingFinished;\n\n  // Accessed only by the loading thread.\n  private boolean tracksEnded;\n  private long sampleOffsetUs;\n  private int chunkUid;\n\n  /**\n   * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.\n   * @param callback A callback for the wrapper.\n   * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained.\n   * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type\n   *     (i.e. {@link DrmInitData#schemeType}). If the stream has {@link DrmInitData} and uses a\n   *     protection scheme type for which overriding {@link DrmInitData} is provided, then the\n   *     stream's {@link DrmInitData} will be overridden.\n   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.\n   * @param positionUs The position from which to start loading media.\n   * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.\n   * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession\n   *     DrmSessions} with.\n   * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n   * @param eventDispatcher A dispatcher to notify of events.\n   */\n  public HlsSampleStreamWrapper(\n      int trackType,\n      Callback callback,\n      HlsChunkSource chunkSource,\n      Map<String, DrmInitData> overridingDrmInitData,\n      Allocator allocator,\n      long positionUs,\n      @Nullable Format muxedAudioFormat,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      @HlsMediaSource.MetadataType int metadataType) {\n    this.trackType = trackType;\n    this.callback = callback;\n    this.chunkSource = chunkSource;\n    this.overridingDrmInitData = overridingDrmInitData;\n    this.allocator = allocator;\n    this.muxedAudioFormat = muxedAudioFormat;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.metadataType = metadataType;\n    loader = new Loader(\"Loader:HlsSampleStreamWrapper\");\n    nextChunkHolder = new HlsChunkSource.HlsChunkHolder();\n    sampleQueueTrackIds = new int[0];\n    sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());\n    sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());\n    sampleQueues = new SampleQueue[0];\n    sampleQueueIsAudioVideoFlags = new boolean[0];\n    sampleQueuesEnabledStates = new boolean[0];\n    mediaChunks = new ArrayList<>();\n    readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);\n    hlsSampleStreams = new ArrayList<>();\n    // Suppressions are needed because `this` is not initialized here.\n    @SuppressWarnings(\"nullness:methodref.receiver.bound.invalid\")\n    Runnable maybeFinishPrepareRunnable = this::maybeFinishPrepare;\n    this.maybeFinishPrepareRunnable = maybeFinishPrepareRunnable;\n    @SuppressWarnings(\"nullness:methodref.receiver.bound.invalid\")\n    Runnable onTracksEndedRunnable = this::onTracksEnded;\n    this.onTracksEndedRunnable = onTracksEndedRunnable;\n    handler = new Handler();\n    lastSeekPositionUs = positionUs;\n    pendingResetPositionUs = positionUs;\n  }\n\n  public void continuePreparing() {\n    if (!prepared) {\n      continueLoading(lastSeekPositionUs);\n    }\n  }\n\n  /**\n   * Prepares the sample stream wrapper with master playlist information.\n   *\n   * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link\n   *     #getTrackGroups()}.\n   * @param primaryTrackGroupIndex The index of the adaptive track group.\n   * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not\n   *     trigger a failure if not found in the media playlist's segments.\n   */\n  public void prepareWithMasterPlaylistInfo(\n      TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) {\n    this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);\n    optionalTrackGroups = new HashSet<>();\n    for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) {\n      optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex));\n    }\n    this.primaryTrackGroupIndex = primaryTrackGroupIndex;\n    handler.post(callback::onPrepared);\n    setIsPrepared();\n  }\n\n  public void maybeThrowPrepareError() throws IOException {\n    maybeThrowError();\n    if (loadingFinished && !prepared) {\n      throw new ParserException(\"Loading finished before preparation is complete.\");\n    }\n  }\n\n  public TrackGroupArray getTrackGroups() {\n    assertIsPrepared();\n    return trackGroups;\n  }\n\n  public int getPrimaryTrackGroupIndex() {\n    return primaryTrackGroupIndex;\n  }\n\n  public int bindSampleQueueToSampleStream(int trackGroupIndex) {\n    assertIsPrepared();\n    Assertions.checkNotNull(trackGroupToSampleQueueIndex);\n\n    int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];\n    if (sampleQueueIndex == C.INDEX_UNSET) {\n      return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex))\n          ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL\n          : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;\n    }\n    if (sampleQueuesEnabledStates[sampleQueueIndex]) {\n      // This sample queue is already bound to a different sample stream.\n      return SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;\n    }\n    sampleQueuesEnabledStates[sampleQueueIndex] = true;\n    return sampleQueueIndex;\n  }\n\n  public void unbindSampleQueue(int trackGroupIndex) {\n    assertIsPrepared();\n    Assertions.checkNotNull(trackGroupToSampleQueueIndex);\n    int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];\n    Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]);\n    sampleQueuesEnabledStates[sampleQueueIndex] = false;\n  }\n\n  /**\n   * Called by the parent {@link HlsMediaPeriod} when a track selection occurs.\n   *\n   * @param selections The renderer track selections.\n   * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained\n   *     for each selection. A {@code true} value indicates that the selection is unchanged, and\n   *     that the caller does not require that the sample stream be recreated.\n   * @param streams The existing sample streams, which will be updated to reflect the provided\n   *     selections.\n   * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that\n   *     have been retained but with the requirement that the consuming renderer be reset.\n   * @param positionUs The current playback position in microseconds.\n   * @param forceReset If true then a reset is forced (i.e. a seek will be performed with in-buffer\n   *     seeking disabled).\n   * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as\n   *     part of the track selection.\n   */\n  public boolean selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs,\n      boolean forceReset) {\n    assertIsPrepared();\n    int oldEnabledTrackGroupCount = enabledTrackGroupCount;\n    // Deselect old tracks.\n    for (int i = 0; i < selections.length; i++) {\n      HlsSampleStream stream = (HlsSampleStream) streams[i];\n      if (stream != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n        enabledTrackGroupCount--;\n        stream.unbindSampleQueue();\n        streams[i] = null;\n      }\n    }\n    // We'll always need to seek if we're being forced to reset, or if this is a first selection to\n    // a position other than the one we started preparing with, or if we're making a selection\n    // having previously disabled all tracks.\n    boolean seekRequired =\n        forceReset\n            || (seenFirstTrackSelection\n                ? oldEnabledTrackGroupCount == 0\n                : positionUs != lastSeekPositionUs);\n    // Get the old (i.e. current before the loop below executes) primary track selection. The new\n    // primary selection will equal the old one unless it's changed in the loop.\n    TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection();\n    TrackSelection primaryTrackSelection = oldPrimaryTrackSelection;\n    // Select new tracks.\n    for (int i = 0; i < selections.length; i++) {\n      TrackSelection selection = selections[i];\n      if (selection == null) {\n        continue;\n      }\n      int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());\n      if (trackGroupIndex == primaryTrackGroupIndex) {\n        primaryTrackSelection = selection;\n        chunkSource.setTrackSelection(selection);\n      }\n      if (streams[i] == null) {\n        enabledTrackGroupCount++;\n        streams[i] = new HlsSampleStream(this, trackGroupIndex);\n        streamResetFlags[i] = true;\n        if (trackGroupToSampleQueueIndex != null) {\n          ((HlsSampleStream) streams[i]).bindSampleQueue();\n          // If there's still a chance of avoiding a seek, try and seek within the sample queue.\n          if (!seekRequired) {\n            SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];\n            sampleQueue.rewind();\n            // A seek can be avoided if we're able to advance to the current playback position in\n            // the sample queue, or if we haven't read anything from the queue since the previous\n            // seek (this case is common for sparse tracks such as metadata tracks). In all other\n            // cases a seek is required.\n            seekRequired =\n                sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED\n                    && sampleQueue.getReadIndex() != 0;\n          }\n        }\n      }\n    }\n\n    if (enabledTrackGroupCount == 0) {\n      chunkSource.reset();\n      downstreamTrackFormat = null;\n      pendingResetUpstreamFormats = true;\n      mediaChunks.clear();\n      if (loader.isLoading()) {\n        if (sampleQueuesBuilt) {\n          // Discard as much as we can synchronously.\n          for (SampleQueue sampleQueue : sampleQueues) {\n            sampleQueue.discardToEnd();\n          }\n        }\n        loader.cancelLoading();\n      } else {\n        resetSampleQueues();\n      }\n    } else {\n      if (!mediaChunks.isEmpty()\n          && !Util.areEqual(primaryTrackSelection, oldPrimaryTrackSelection)) {\n        // The primary track selection has changed and we have buffered media. The buffered media\n        // may need to be discarded.\n        boolean primarySampleQueueDirty = false;\n        if (!seenFirstTrackSelection) {\n          long bufferedDurationUs = positionUs < 0 ? -positionUs : 0;\n          HlsMediaChunk lastMediaChunk = getLastMediaChunk();\n          MediaChunkIterator[] mediaChunkIterators =\n              chunkSource.createMediaChunkIterators(lastMediaChunk, positionUs);\n          primaryTrackSelection.updateSelectedTrack(\n              positionUs,\n              bufferedDurationUs,\n              C.TIME_UNSET,\n              readOnlyMediaChunks,\n              mediaChunkIterators);\n          int chunkIndex = chunkSource.getTrackGroup().indexOf(lastMediaChunk.trackFormat);\n          if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {\n            // This is the first selection and the chunk loaded during preparation does not match\n            // the initially selected format.\n            primarySampleQueueDirty = true;\n          }\n        } else {\n          // The primary sample queue contains media buffered for the old primary track selection.\n          primarySampleQueueDirty = true;\n        }\n        if (primarySampleQueueDirty) {\n          forceReset = true;\n          seekRequired = true;\n          pendingResetUpstreamFormats = true;\n        }\n      }\n      if (seekRequired) {\n        seekToUs(positionUs, forceReset);\n        // We'll need to reset renderers consuming from all streams due to the seek.\n        for (int i = 0; i < streams.length; i++) {\n          if (streams[i] != null) {\n            streamResetFlags[i] = true;\n          }\n        }\n      }\n    }\n\n    updateSampleStreams(streams);\n    seenFirstTrackSelection = true;\n    return seekRequired;\n  }\n\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    if (!sampleQueuesBuilt || isPendingReset()) {\n      return;\n    }\n    int sampleQueueCount = sampleQueues.length;\n    for (int i = 0; i < sampleQueueCount; i++) {\n      sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]);\n    }\n  }\n\n  /**\n   * Attempts to seek to the specified position in microseconds.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @param forceReset If true then a reset is forced (i.e. in-buffer seeking is disabled).\n   * @return Whether the wrapper was reset, meaning the wrapped sample queues were reset. If false,\n   *     an in-buffer seek was performed.\n   */\n  public boolean seekToUs(long positionUs, boolean forceReset) {\n    lastSeekPositionUs = positionUs;\n    if (isPendingReset()) {\n      // A reset is already pending. We only need to update its position.\n      pendingResetPositionUs = positionUs;\n      return true;\n    }\n\n    // If we're not forced to reset, try and seek within the buffer.\n    if (sampleQueuesBuilt && !forceReset && seekInsideBufferUs(positionUs)) {\n      return false;\n    }\n\n    // We can't seek inside the buffer, and so need to reset.\n    pendingResetPositionUs = positionUs;\n    loadingFinished = false;\n    mediaChunks.clear();\n    if (loader.isLoading()) {\n      loader.cancelLoading();\n    } else {\n      loader.clearFatalError();\n      resetSampleQueues();\n    }\n    return true;\n  }\n\n  public void release() {\n    if (prepared) {\n      // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise\n      // sampleQueues may still be being modified by the loading thread.\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.preRelease();\n      }\n    }\n    loader.release(this);\n    handler.removeCallbacksAndMessages(null);\n    released = true;\n    hlsSampleStreams.clear();\n  }\n\n  @Override\n  public void onLoaderReleased() {\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.release();\n    }\n  }\n\n  public void setIsTimestampMaster(boolean isTimestampMaster) {\n    chunkSource.setIsTimestampMaster(isTimestampMaster);\n  }\n\n  public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) {\n    return chunkSource.onPlaylistError(playlistUrl, blacklistDurationMs);\n  }\n\n  // SampleStream implementation.\n\n  public boolean isReady(int sampleQueueIndex) {\n    return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished);\n  }\n\n  public void maybeThrowError(int sampleQueueIndex) throws IOException {\n    maybeThrowError();\n    sampleQueues[sampleQueueIndex].maybeThrowError();\n  }\n\n  public void maybeThrowError() throws IOException {\n    loader.maybeThrowError();\n    chunkSource.maybeThrowError();\n  }\n\n  public int readData(int sampleQueueIndex, FormatHolder formatHolder, DecoderInputBuffer buffer,\n      boolean requireFormat) {\n    if (isPendingReset()) {\n      return C.RESULT_NOTHING_READ;\n    }\n\n    // TODO: Split into discard (in discardBuffer) and format change (here and in skipData) steps.\n    if (!mediaChunks.isEmpty()) {\n      int discardToMediaChunkIndex = 0;\n      while (discardToMediaChunkIndex < mediaChunks.size() - 1\n          && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) {\n        discardToMediaChunkIndex++;\n      }\n      Util.removeRange(mediaChunks, 0, discardToMediaChunkIndex);\n      HlsMediaChunk currentChunk = mediaChunks.get(0);\n      Format trackFormat = currentChunk.trackFormat;\n      if (!trackFormat.equals(downstreamTrackFormat)) {\n        eventDispatcher.downstreamFormatChanged(trackType, trackFormat,\n            currentChunk.trackSelectionReason, currentChunk.trackSelectionData,\n            currentChunk.startTimeUs);\n      }\n      downstreamTrackFormat = trackFormat;\n    }\n\n    int result =\n        sampleQueues[sampleQueueIndex].read(\n            formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs);\n    if (result == C.RESULT_FORMAT_READ) {\n      Format format = Assertions.checkNotNull(formatHolder.format);\n      if (sampleQueueIndex == primarySampleQueueIndex) {\n        // Fill in primary sample format with information from the track format.\n        int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId();\n        int chunkIndex = 0;\n        while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) {\n          chunkIndex++;\n        }\n        Format trackFormat =\n            chunkIndex < mediaChunks.size()\n                ? mediaChunks.get(chunkIndex).trackFormat\n                : Assertions.checkNotNull(upstreamTrackFormat);\n        format = format.copyWithManifestFormatInfo(trackFormat);\n      }\n      formatHolder.format = format;\n    }\n    return result;\n  }\n\n  public int skipData(int sampleQueueIndex, long positionUs) {\n    if (isPendingReset()) {\n      return 0;\n    }\n\n    SampleQueue sampleQueue = sampleQueues[sampleQueueIndex];\n    if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {\n      return sampleQueue.advanceToEnd();\n    } else {\n      int skipCount = sampleQueue.advanceTo(positionUs, true, true);\n      return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;\n    }\n  }\n\n  // SequenceableLoader implementation\n\n  @Override\n  public long getBufferedPositionUs() {\n    if (loadingFinished) {\n      return C.TIME_END_OF_SOURCE;\n    } else if (isPendingReset()) {\n      return pendingResetPositionUs;\n    } else {\n      long bufferedPositionUs = lastSeekPositionUs;\n      HlsMediaChunk lastMediaChunk = getLastMediaChunk();\n      HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk\n          : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;\n      if (lastCompletedMediaChunk != null) {\n        bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);\n      }\n      if (sampleQueuesBuilt) {\n        for (SampleQueue sampleQueue : sampleQueues) {\n          bufferedPositionUs =\n              Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs());\n        }\n      }\n      return bufferedPositionUs;\n    }\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    if (isPendingReset()) {\n      return pendingResetPositionUs;\n    } else {\n      return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;\n    }\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {\n      return false;\n    }\n\n    List<HlsMediaChunk> chunkQueue;\n    long loadPositionUs;\n    if (isPendingReset()) {\n      chunkQueue = Collections.emptyList();\n      loadPositionUs = pendingResetPositionUs;\n    } else {\n      chunkQueue = readOnlyMediaChunks;\n      HlsMediaChunk lastMediaChunk = getLastMediaChunk();\n      loadPositionUs =\n          lastMediaChunk.isLoadCompleted()\n              ? lastMediaChunk.endTimeUs\n              : Math.max(lastSeekPositionUs, lastMediaChunk.startTimeUs);\n    }\n    chunkSource.getNextChunk(\n        positionUs,\n        loadPositionUs,\n        chunkQueue,\n        /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(),\n        nextChunkHolder);\n    boolean endOfStream = nextChunkHolder.endOfStream;\n    Chunk loadable = nextChunkHolder.chunk;\n    Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;\n    nextChunkHolder.clear();\n\n    if (endOfStream) {\n      pendingResetPositionUs = C.TIME_UNSET;\n      loadingFinished = true;\n      return true;\n    }\n\n    if (loadable == null) {\n      if (playlistUrlToLoad != null) {\n        callback.onPlaylistRefreshRequired(playlistUrlToLoad);\n      }\n      return false;\n    }\n\n    if (isMediaChunk(loadable)) {\n      pendingResetPositionUs = C.TIME_UNSET;\n      HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;\n      mediaChunk.init(this);\n      mediaChunks.add(mediaChunk);\n      upstreamTrackFormat = mediaChunk.trackFormat;\n    }\n    long elapsedRealtimeMs =\n        loader.startLoading(\n            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));\n    eventDispatcher.loadStarted(\n        loadable.dataSpec,\n        loadable.type,\n        trackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs);\n    return true;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return loader.isLoading();\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    // Do nothing.\n  }\n\n  // Loader.Callback implementation.\n\n  @Override\n  public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {\n    chunkSource.onChunkLoadCompleted(loadable);\n    eventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        trackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    if (!prepared) {\n      continueLoading(lastSeekPositionUs);\n    } else {\n      callback.onContinueLoadingRequested(this);\n    }\n  }\n\n  @Override\n  public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,\n      boolean released) {\n    eventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        trackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    if (!released) {\n      resetSampleQueues();\n      if (enabledTrackGroupCount > 0) {\n        callback.onContinueLoadingRequested(this);\n      }\n    }\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      Chunk loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long bytesLoaded = loadable.bytesLoaded();\n    boolean isMediaChunk = isMediaChunk(loadable);\n    boolean blacklistSucceeded = false;\n    LoadErrorAction loadErrorAction;\n\n    long blacklistDurationMs =\n        loadErrorHandlingPolicy.getBlacklistDurationMsFor(\n            loadable.type, loadDurationMs, error, errorCount);\n    if (blacklistDurationMs != C.TIME_UNSET) {\n      blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs);\n    }\n\n    if (blacklistSucceeded) {\n      if (isMediaChunk && bytesLoaded == 0) {\n        HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1);\n        Assertions.checkState(removed == loadable);\n        if (mediaChunks.isEmpty()) {\n          pendingResetPositionUs = lastSeekPositionUs;\n        }\n      }\n      loadErrorAction = Loader.DONT_RETRY;\n    } else /* did not blacklist */ {\n      long retryDelayMs =\n          loadErrorHandlingPolicy.getRetryDelayMsFor(\n              loadable.type, loadDurationMs, error, errorCount);\n      loadErrorAction =\n          retryDelayMs != C.TIME_UNSET\n              ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs)\n              : Loader.DONT_RETRY_FATAL;\n    }\n\n    eventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        trackType,\n        loadable.trackFormat,\n        loadable.trackSelectionReason,\n        loadable.trackSelectionData,\n        loadable.startTimeUs,\n        loadable.endTimeUs,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        bytesLoaded,\n        error,\n        /* wasCanceled= */ !loadErrorAction.isRetry());\n\n    if (blacklistSucceeded) {\n      if (!prepared) {\n        continueLoading(lastSeekPositionUs);\n      } else {\n        callback.onContinueLoadingRequested(this);\n      }\n    }\n    return loadErrorAction;\n  }\n\n  // Called by the consuming thread, but only when there is no loading thread.\n\n  /**\n   * Initializes the wrapper for loading a chunk.\n   *\n   * @param chunkUid The chunk's uid.\n   * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any\n   *     samples already queued to the wrapper.\n   * @param reusingExtractor Whether the extractor for the chunk has already been used for preceding\n   *     chunks.\n   */\n  public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) {\n    if (!reusingExtractor) {\n      sampleQueueMappingDoneByType.clear();\n    }\n    this.chunkUid = chunkUid;\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.sourceId(chunkUid);\n    }\n    if (shouldSpliceIn) {\n      for (SampleQueue sampleQueue : sampleQueues) {\n        sampleQueue.splice();\n      }\n    }\n  }\n\n  // ExtractorOutput implementation. Called by the loading thread.\n\n  @Override\n  public TrackOutput track(int id, int type) {\n    @Nullable TrackOutput trackOutput = null;\n    if (MAPPABLE_TYPES.contains(type)) {\n      // Track types in MAPPABLE_TYPES are handled manually to ignore IDs.\n      trackOutput = getMappedTrackOutput(id, type);\n    } else /* non-mappable type track */ {\n      for (int i = 0; i < sampleQueues.length; i++) {\n        if (sampleQueueTrackIds[i] == id) {\n          trackOutput = sampleQueues[i];\n          break;\n        }\n      }\n    }\n\n    if (trackOutput == null) {\n      if (tracksEnded) {\n        return createDummyTrackOutput(id, type);\n      } else {\n        // The relevant SampleQueue hasn't been constructed yet - so construct it.\n        trackOutput = createSampleQueue(id, type);\n      }\n    }\n\n    if (type == C.TRACK_TYPE_METADATA) {\n      if (emsgUnwrappingTrackOutput == null) {\n        emsgUnwrappingTrackOutput = new EmsgUnwrappingTrackOutput(trackOutput, metadataType);\n      }\n      return emsgUnwrappingTrackOutput;\n    }\n    return trackOutput;\n  }\n\n  /**\n   * Returns the {@link TrackOutput} for the provided {@code type} and {@code id}, or null if none\n   * has been created yet.\n   *\n   * <p>If a {@link SampleQueue} for {@code type} has been created and is mapped, but it has a\n   * different ID, then return a {@link DummyTrackOutput} that does nothing.\n   *\n   * <p>If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to\n   * this {@code id} and return it. This situation can happen after a call to {@link #init} with\n   * {@code reusingExtractor=false}.\n   *\n   * @param id The ID of the track.\n   * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}.\n   * @return The the mapped {@link TrackOutput}, or null if it's not been created yet.\n   */\n  @Nullable\n  private TrackOutput getMappedTrackOutput(int id, int type) {\n    Assertions.checkArgument(MAPPABLE_TYPES.contains(type));\n    int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET);\n    if (sampleQueueIndex == C.INDEX_UNSET) {\n      return null;\n    }\n\n    if (sampleQueueMappingDoneByType.add(type)) {\n      sampleQueueTrackIds[sampleQueueIndex] = id;\n    }\n    return sampleQueueTrackIds[sampleQueueIndex] == id\n        ? sampleQueues[sampleQueueIndex]\n        : createDummyTrackOutput(id, type);\n  }\n\n  private SampleQueue createSampleQueue(int id, int type) {\n    int trackCount = sampleQueues.length;\n\n    SampleQueue trackOutput =\n        new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData);\n    trackOutput.setSampleOffsetUs(sampleOffsetUs);\n    trackOutput.sourceId(chunkUid);\n    trackOutput.setUpstreamFormatChangeListener(this);\n    sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);\n    sampleQueueTrackIds[trackCount] = id;\n    sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);\n    sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);\n    sampleQueueIsAudioVideoFlags[trackCount] =\n        type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;\n    haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];\n    sampleQueueMappingDoneByType.add(type);\n    sampleQueueIndicesByType.append(type, trackCount);\n    if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) {\n      primarySampleQueueIndex = trackCount;\n      primarySampleQueueType = type;\n    }\n    sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1);\n    return trackOutput;\n  }\n\n  @Override\n  public void endTracks() {\n    tracksEnded = true;\n    handler.post(onTracksEndedRunnable);\n  }\n\n  @Override\n  public void seekMap(SeekMap seekMap) {\n    // Do nothing.\n  }\n\n  // UpstreamFormatChangedListener implementation. Called by the loading thread.\n\n  @Override\n  public void onUpstreamFormatChanged(Format format) {\n    handler.post(maybeFinishPrepareRunnable);\n  }\n\n  // Called by the loading thread.\n\n  public void setSampleOffsetUs(long sampleOffsetUs) {\n    this.sampleOffsetUs = sampleOffsetUs;\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.setSampleOffsetUs(sampleOffsetUs);\n    }\n  }\n\n  // Internal methods.\n\n  private void updateSampleStreams(@NullableType SampleStream[] streams) {\n    hlsSampleStreams.clear();\n    for (SampleStream stream : streams) {\n      if (stream != null) {\n        hlsSampleStreams.add((HlsSampleStream) stream);\n      }\n    }\n  }\n\n  private boolean finishedReadingChunk(HlsMediaChunk chunk) {\n    int chunkUid = chunk.uid;\n    int sampleQueueCount = sampleQueues.length;\n    for (int i = 0; i < sampleQueueCount; i++) {\n      if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private void resetSampleQueues() {\n    for (SampleQueue sampleQueue : sampleQueues) {\n      sampleQueue.reset(pendingResetUpstreamFormats);\n    }\n    pendingResetUpstreamFormats = false;\n  }\n\n  private void onTracksEnded() {\n    sampleQueuesBuilt = true;\n    maybeFinishPrepare();\n  }\n\n  private void maybeFinishPrepare() {\n    if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) {\n      return;\n    }\n    for (SampleQueue sampleQueue : sampleQueues) {\n      if (sampleQueue.getUpstreamFormat() == null) {\n        return;\n      }\n    }\n    if (trackGroups != null) {\n      // The track groups were created with master playlist information. They only need to be mapped\n      // to a sample queue.\n      mapSampleQueuesToMatchTrackGroups();\n    } else {\n      // Tracks are created using media segment information.\n      buildTracksFromSampleStreams();\n      setIsPrepared();\n      callback.onPrepared();\n    }\n  }\n\n  @RequiresNonNull(\"trackGroups\")\n  @EnsuresNonNull(\"trackGroupToSampleQueueIndex\")\n  private void mapSampleQueuesToMatchTrackGroups() {\n    int trackGroupCount = trackGroups.length;\n    trackGroupToSampleQueueIndex = new int[trackGroupCount];\n    Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET);\n    for (int i = 0; i < trackGroupCount; i++) {\n      for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) {\n        SampleQueue sampleQueue = sampleQueues[queueIndex];\n        if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) {\n          trackGroupToSampleQueueIndex[i] = queueIndex;\n          break;\n        }\n      }\n    }\n    for (HlsSampleStream sampleStream : hlsSampleStreams) {\n      sampleStream.bindSampleQueue();\n    }\n  }\n\n  /**\n   * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as\n   * internal data-structures required for operation.\n   *\n   * <p>Tracks in HLS are complicated. A HLS master playlist contains a number of \"variants\". Each\n   * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata\n   * and caption tracks. We wish to allow the user to select between an adaptive track that spans\n   * all variants, as well as each individual variant. If multiple audio tracks are present within\n   * each variant then we wish to allow the user to select between those also.\n   *\n   * <p>To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1)\n   * tracks, where N is the number of variants defined in the HLS master playlist. These consist of\n   * one adaptive track defined to span all variants and a track for each individual variant. The\n   * adaptive track is initially selected. The extractor is then prepared to discover the tracks\n   * inside of each variant stream. The two sets of tracks are then combined by this method to\n   * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}:\n   *\n   * <ul>\n   *   <li>The extractor tracks are inspected to infer a \"primary\" track type. If a video track is\n   *       present then it is always the primary type. If not, audio is the primary type if present.\n   *       Else text is the primary type if present. Else there is no primary type.\n   *   <li>If there is exactly one extractor track of the primary type, it's expanded into (N+1)\n   *       exposed tracks, all of which correspond to the primary extractor track and each of which\n   *       corresponds to a different chunk source track. Selecting one of these tracks has the\n   *       effect of switching the selected track on the chunk source.\n   *   <li>All other extractor tracks are exposed directly. Selecting one of these tracks has the\n   *       effect of selecting an extractor track, leaving the selected track on the chunk source\n   *       unchanged.\n   * </ul>\n   */\n  @EnsuresNonNull({\"trackGroups\", \"optionalTrackGroups\", \"trackGroupToSampleQueueIndex\"})\n  private void buildTracksFromSampleStreams() {\n    // Iterate through the extractor tracks to discover the \"primary\" track type, and the index\n    // of the single track of this type.\n    int primaryExtractorTrackType = C.TRACK_TYPE_NONE;\n    int primaryExtractorTrackIndex = C.INDEX_UNSET;\n    int extractorTrackCount = sampleQueues.length;\n    for (int i = 0; i < extractorTrackCount; i++) {\n      String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType;\n      int trackType;\n      if (MimeTypes.isVideo(sampleMimeType)) {\n        trackType = C.TRACK_TYPE_VIDEO;\n      } else if (MimeTypes.isAudio(sampleMimeType)) {\n        trackType = C.TRACK_TYPE_AUDIO;\n      } else if (MimeTypes.isText(sampleMimeType)) {\n        trackType = C.TRACK_TYPE_TEXT;\n      } else {\n        trackType = C.TRACK_TYPE_NONE;\n      }\n      if (getTrackTypeScore(trackType) > getTrackTypeScore(primaryExtractorTrackType)) {\n        primaryExtractorTrackType = trackType;\n        primaryExtractorTrackIndex = i;\n      } else if (trackType == primaryExtractorTrackType\n          && primaryExtractorTrackIndex != C.INDEX_UNSET) {\n        // We have multiple tracks of the primary type. We only want an index if there only exists a\n        // single track of the primary type, so unset the index again.\n        primaryExtractorTrackIndex = C.INDEX_UNSET;\n      }\n    }\n\n    TrackGroup chunkSourceTrackGroup = chunkSource.getTrackGroup();\n    int chunkSourceTrackCount = chunkSourceTrackGroup.length;\n\n    // Instantiate the necessary internal data-structures.\n    primaryTrackGroupIndex = C.INDEX_UNSET;\n    trackGroupToSampleQueueIndex = new int[extractorTrackCount];\n    for (int i = 0; i < extractorTrackCount; i++) {\n      trackGroupToSampleQueueIndex[i] = i;\n    }\n\n    // Construct the set of exposed track groups.\n    TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];\n    for (int i = 0; i < extractorTrackCount; i++) {\n      Format sampleFormat = sampleQueues[i].getUpstreamFormat();\n      if (i == primaryExtractorTrackIndex) {\n        Format[] formats = new Format[chunkSourceTrackCount];\n        if (chunkSourceTrackCount == 1) {\n          formats[0] = sampleFormat.copyWithManifestFormatInfo(chunkSourceTrackGroup.getFormat(0));\n        } else {\n          for (int j = 0; j < chunkSourceTrackCount; j++) {\n            formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true);\n          }\n        }\n        trackGroups[i] = new TrackGroup(formats);\n        primaryTrackGroupIndex = i;\n      } else {\n        Format trackFormat =\n            primaryExtractorTrackType == C.TRACK_TYPE_VIDEO\n                    && MimeTypes.isAudio(sampleFormat.sampleMimeType)\n                ? muxedAudioFormat\n                : null;\n        trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false));\n      }\n    }\n    this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);\n    Assertions.checkState(optionalTrackGroups == null);\n    optionalTrackGroups = Collections.emptySet();\n  }\n\n  private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) {\n    for (int i = 0; i < trackGroups.length; i++) {\n      TrackGroup trackGroup = trackGroups[i];\n      Format[] exposedFormats = new Format[trackGroup.length];\n      for (int j = 0; j < trackGroup.length; j++) {\n        Format format = trackGroup.getFormat(j);\n        if (format.drmInitData != null) {\n          format =\n              format.copyWithExoMediaCryptoType(\n                  drmSessionManager.getExoMediaCryptoType(format.drmInitData));\n        }\n        exposedFormats[j] = format;\n      }\n      trackGroups[i] = new TrackGroup(exposedFormats);\n    }\n    return new TrackGroupArray(trackGroups);\n  }\n\n  private HlsMediaChunk getLastMediaChunk() {\n    return mediaChunks.get(mediaChunks.size() - 1);\n  }\n\n  private boolean isPendingReset() {\n    return pendingResetPositionUs != C.TIME_UNSET;\n  }\n\n  /**\n   * Attempts to seek to the specified position within the sample queues.\n   *\n   * @param positionUs The seek position in microseconds.\n   * @return Whether the in-buffer seek was successful.\n   */\n  private boolean seekInsideBufferUs(long positionUs) {\n    int sampleQueueCount = sampleQueues.length;\n    for (int i = 0; i < sampleQueueCount; i++) {\n      SampleQueue sampleQueue = sampleQueues[i];\n      sampleQueue.rewind();\n      boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)\n          != SampleQueue.ADVANCE_FAILED;\n      // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue\n      // is successful. We ignore whether seeks within non-AV queues are successful in this case, as\n      // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is\n      // successful only if the seek into every queue succeeds.\n      if (!seekInsideQueue && (sampleQueueIsAudioVideoFlags[i] || !haveAudioVideoSampleQueues)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  @RequiresNonNull({\"trackGroups\", \"optionalTrackGroups\"})\n  private void setIsPrepared() {\n    prepared = true;\n  }\n\n  @EnsuresNonNull({\"trackGroups\", \"optionalTrackGroups\"})\n  private void assertIsPrepared() {\n    Assertions.checkState(prepared);\n    Assertions.checkNotNull(trackGroups);\n    Assertions.checkNotNull(optionalTrackGroups);\n  }\n\n  /**\n   * Scores a track type. Where multiple tracks are muxed into a container, the track with the\n   * highest score is the primary track.\n   *\n   * @param trackType The track type.\n   * @return The score.\n   */\n  private static int getTrackTypeScore(int trackType) {\n    switch (trackType) {\n      case C.TRACK_TYPE_VIDEO:\n        return 3;\n      case C.TRACK_TYPE_AUDIO:\n        return 2;\n      case C.TRACK_TYPE_TEXT:\n        return 1;\n      default:\n        return 0;\n    }\n  }\n\n  /**\n   * Derives a track sample format from the corresponding format in the master playlist, and a\n   * sample format that may have been obtained from a chunk belonging to a different track.\n   *\n   * @param playlistFormat The format information obtained from the master playlist.\n   * @param sampleFormat The format information obtained from the samples.\n   * @param propagateBitrate Whether the bitrate from the playlist format should be included in the\n   *     derived format.\n   * @return The derived track format.\n   */\n  private static Format deriveFormat(\n      @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrate) {\n    if (playlistFormat == null) {\n      return sampleFormat;\n    }\n    int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE;\n    int channelCount =\n        playlistFormat.channelCount != Format.NO_VALUE\n            ? playlistFormat.channelCount\n            : sampleFormat.channelCount;\n    int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);\n    String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);\n    String mimeType = MimeTypes.getMediaMimeType(codecs);\n    if (mimeType == null) {\n      mimeType = sampleFormat.sampleMimeType;\n    }\n    return sampleFormat.copyWithContainerInfo(\n        playlistFormat.id,\n        playlistFormat.label,\n        mimeType,\n        codecs,\n        playlistFormat.metadata,\n        bitrate,\n        playlistFormat.width,\n        playlistFormat.height,\n        channelCount,\n        playlistFormat.selectionFlags,\n        playlistFormat.language);\n  }\n\n  private static boolean isMediaChunk(Chunk chunk) {\n    return chunk instanceof HlsMediaChunk;\n  }\n\n  private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) {\n    String manifestFormatMimeType = manifestFormat.sampleMimeType;\n    String sampleFormatMimeType = sampleFormat.sampleMimeType;\n    int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType);\n    if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) {\n      return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType);\n    } else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) {\n      return false;\n    }\n    if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType)\n        || MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) {\n      return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel;\n    }\n    return true;\n  }\n\n  private static DummyTrackOutput createDummyTrackOutput(int id, int type) {\n    Log.w(TAG, \"Unmapped track with id \" + id + \" of type \" + type);\n    return new DummyTrackOutput();\n  }\n\n  private static final class FormatAdjustingSampleQueue extends SampleQueue {\n\n    private final Map<String, DrmInitData> overridingDrmInitData;\n\n    public FormatAdjustingSampleQueue(\n        Allocator allocator,\n        DrmSessionManager<?> drmSessionManager,\n        Map<String, DrmInitData> overridingDrmInitData) {\n      super(allocator, drmSessionManager);\n      this.overridingDrmInitData = overridingDrmInitData;\n    }\n\n    @Override\n    public void format(Format format) {\n      DrmInitData drmInitData = format.drmInitData;\n      if (drmInitData != null) {\n        DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);\n        if (overridingDrmInitData != null) {\n          drmInitData = overridingDrmInitData;\n        }\n      }\n      super.format(format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata)));\n    }\n\n    /**\n     * Strips the private timestamp frame from metadata, if present. See:\n     * https://github.com/google/ExoPlayer/issues/5063\n     */\n    @Nullable\n    private Metadata getAdjustedMetadata(@Nullable Metadata metadata) {\n      if (metadata == null) {\n        return null;\n      }\n      int length = metadata.length();\n      int transportStreamTimestampMetadataIndex = C.INDEX_UNSET;\n      for (int i = 0; i < length; i++) {\n        Metadata.Entry metadataEntry = metadata.get(i);\n        if (metadataEntry instanceof PrivFrame) {\n          PrivFrame privFrame = (PrivFrame) metadataEntry;\n          if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {\n            transportStreamTimestampMetadataIndex = i;\n            break;\n          }\n        }\n      }\n      if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) {\n        return metadata;\n      }\n      if (length == 1) {\n        return null;\n      }\n      Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1];\n      for (int i = 0; i < length; i++) {\n        if (i != transportStreamTimestampMetadataIndex) {\n          int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1;\n          newMetadataEntries[newIndex] = metadata.get(i);\n        }\n      }\n      return new Metadata(newMetadataEntries);\n    }\n  }\n\n  private static class EmsgUnwrappingTrackOutput implements TrackOutput {\n\n    private static final String TAG = \"EmsgUnwrappingTrackOutput\";\n\n    // TODO(ibaker): Create a Formats util class with common constants like this.\n    private static final Format ID3_FORMAT =\n        Format.createSampleFormat(\n            /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE);\n    private static final Format EMSG_FORMAT =\n        Format.createSampleFormat(\n            /* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);\n\n    private final EventMessageDecoder emsgDecoder;\n    private final TrackOutput delegate;\n    private final Format delegateFormat;\n    @MonotonicNonNull private Format format;\n\n    private byte[] buffer;\n    private int bufferPosition;\n\n    public EmsgUnwrappingTrackOutput(\n        TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) {\n      this.emsgDecoder = new EventMessageDecoder();\n      this.delegate = delegate;\n      switch (metadataType) {\n        case HlsMediaSource.METADATA_TYPE_ID3:\n          delegateFormat = ID3_FORMAT;\n          break;\n        case HlsMediaSource.METADATA_TYPE_EMSG:\n          delegateFormat = EMSG_FORMAT;\n          break;\n        default:\n          throw new IllegalArgumentException(\"Unknown metadataType: \" + metadataType);\n      }\n\n      this.buffer = new byte[0];\n      this.bufferPosition = 0;\n    }\n\n    @Override\n    public void format(Format format) {\n      this.format = format;\n      delegate.format(delegateFormat);\n    }\n\n    @Override\n    public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n        throws IOException, InterruptedException {\n      ensureBufferCapacity(bufferPosition + length);\n      int numBytesRead = input.read(buffer, bufferPosition, length);\n      if (numBytesRead == C.RESULT_END_OF_INPUT) {\n        if (allowEndOfInput) {\n          return C.RESULT_END_OF_INPUT;\n        } else {\n          throw new EOFException();\n        }\n      }\n      bufferPosition += numBytesRead;\n      return numBytesRead;\n    }\n\n    @Override\n    public void sampleData(ParsableByteArray buffer, int length) {\n      ensureBufferCapacity(bufferPosition + length);\n      buffer.readBytes(this.buffer, bufferPosition, length);\n      bufferPosition += length;\n    }\n\n    @Override\n    public void sampleMetadata(\n        long timeUs,\n        @C.BufferFlags int flags,\n        int size,\n        int offset,\n        @Nullable CryptoData cryptoData) {\n      Assertions.checkNotNull(format);\n      ParsableByteArray sample = getSampleAndTrimBuffer(size, offset);\n      ParsableByteArray sampleForDelegate;\n      if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) {\n        // Incoming format matches delegate track's format, so pass straight through.\n        sampleForDelegate = sample;\n      } else if (MimeTypes.APPLICATION_EMSG.equals(format.sampleMimeType)) {\n        // Incoming sample is EMSG, and delegate track is not expecting EMSG, so try unwrapping.\n        EventMessage emsg = emsgDecoder.decode(sample);\n        if (!emsgContainsExpectedWrappedFormat(emsg)) {\n          Log.w(\n              TAG,\n              String.format(\n                  \"Ignoring EMSG. Expected it to contain wrapped %s but actual wrapped format: %s\",\n                  delegateFormat.sampleMimeType, emsg.getWrappedMetadataFormat()));\n          return;\n        }\n        sampleForDelegate =\n            new ParsableByteArray(Assertions.checkNotNull(emsg.getWrappedMetadataBytes()));\n      } else {\n        Log.w(TAG, \"Ignoring sample for unsupported format: \" + format.sampleMimeType);\n        return;\n      }\n\n      int sampleSize = sampleForDelegate.bytesLeft();\n\n      delegate.sampleData(sampleForDelegate, sampleSize);\n      delegate.sampleMetadata(timeUs, flags, sampleSize, offset, cryptoData);\n    }\n\n    private boolean emsgContainsExpectedWrappedFormat(EventMessage emsg) {\n      @Nullable Format wrappedMetadataFormat = emsg.getWrappedMetadataFormat();\n      return wrappedMetadataFormat != null\n          && Util.areEqual(delegateFormat.sampleMimeType, wrappedMetadataFormat.sampleMimeType);\n    }\n\n    private void ensureBufferCapacity(int requiredLength) {\n      if (buffer.length < requiredLength) {\n        buffer = Arrays.copyOf(buffer, requiredLength + requiredLength / 2);\n      }\n    }\n\n    /**\n     * Removes a complete sample from the {@link #buffer} field & reshuffles the tail data skipped\n     * by {@code offset} to the head of the array.\n     *\n     * @param size see {@code size} param of {@link #sampleMetadata}.\n     * @param offset see {@code offset} param of {@link #sampleMetadata}.\n     * @return A {@link ParsableByteArray} containing the sample removed from {@link #buffer}.\n     */\n    private ParsableByteArray getSampleAndTrimBuffer(int size, int offset) {\n      int sampleEnd = bufferPosition - offset;\n      int sampleStart = sampleEnd - size;\n\n      byte[] sampleBytes = Arrays.copyOfRange(buffer, sampleStart, sampleEnd);\n      ParsableByteArray sample = new ParsableByteArray(sampleBytes);\n\n      System.arraycopy(buffer, sampleEnd, buffer, 0, offset);\n      bufferPosition = offset;\n      return sample;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Holds metadata associated to an HLS media track. */\npublic final class HlsTrackMetadataEntry implements Metadata.Entry {\n\n  /** Holds attributes defined in an EXT-X-STREAM-INF tag. */\n  public static final class VariantInfo implements Parcelable {\n\n    /** The bitrate as declared by the EXT-X-STREAM-INF tag. */\n    public final long bitrate;\n\n    /**\n     * The VIDEO value as defined in the EXT-X-STREAM-INF tag, or null if the VIDEO attribute is not\n     * present.\n     */\n    @Nullable public final String videoGroupId;\n\n    /**\n     * The AUDIO value as defined in the EXT-X-STREAM-INF tag, or null if the AUDIO attribute is not\n     * present.\n     */\n    @Nullable public final String audioGroupId;\n\n    /**\n     * The SUBTITLES value as defined in the EXT-X-STREAM-INF tag, or null if the SUBTITLES\n     * attribute is not present.\n     */\n    @Nullable public final String subtitleGroupId;\n\n    /**\n     * The CLOSED-CAPTIONS value as defined in the EXT-X-STREAM-INF tag, or null if the\n     * CLOSED-CAPTIONS attribute is not present.\n     */\n    @Nullable public final String captionGroupId;\n\n    /**\n     * Creates an instance.\n     *\n     * @param bitrate See {@link #bitrate}.\n     * @param videoGroupId See {@link #videoGroupId}.\n     * @param audioGroupId See {@link #audioGroupId}.\n     * @param subtitleGroupId See {@link #subtitleGroupId}.\n     * @param captionGroupId See {@link #captionGroupId}.\n     */\n    public VariantInfo(\n        long bitrate,\n        @Nullable String videoGroupId,\n        @Nullable String audioGroupId,\n        @Nullable String subtitleGroupId,\n        @Nullable String captionGroupId) {\n      this.bitrate = bitrate;\n      this.videoGroupId = videoGroupId;\n      this.audioGroupId = audioGroupId;\n      this.subtitleGroupId = subtitleGroupId;\n      this.captionGroupId = captionGroupId;\n    }\n\n    /* package */ VariantInfo(Parcel in) {\n      bitrate = in.readLong();\n      videoGroupId = in.readString();\n      audioGroupId = in.readString();\n      subtitleGroupId = in.readString();\n      captionGroupId = in.readString();\n    }\n\n    @Override\n    public boolean equals(@Nullable Object other) {\n      if (this == other) {\n        return true;\n      }\n      if (other == null || getClass() != other.getClass()) {\n        return false;\n      }\n      VariantInfo that = (VariantInfo) other;\n      return bitrate == that.bitrate\n          && TextUtils.equals(videoGroupId, that.videoGroupId)\n          && TextUtils.equals(audioGroupId, that.audioGroupId)\n          && TextUtils.equals(subtitleGroupId, that.subtitleGroupId)\n          && TextUtils.equals(captionGroupId, that.captionGroupId);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = (int) (bitrate ^ (bitrate >>> 32));\n      result = 31 * result + (videoGroupId != null ? videoGroupId.hashCode() : 0);\n      result = 31 * result + (audioGroupId != null ? audioGroupId.hashCode() : 0);\n      result = 31 * result + (subtitleGroupId != null ? subtitleGroupId.hashCode() : 0);\n      result = 31 * result + (captionGroupId != null ? captionGroupId.hashCode() : 0);\n      return result;\n    }\n\n    // Parcelable implementation.\n\n    @Override\n    public int describeContents() {\n      return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n      dest.writeLong(bitrate);\n      dest.writeString(videoGroupId);\n      dest.writeString(audioGroupId);\n      dest.writeString(subtitleGroupId);\n      dest.writeString(captionGroupId);\n    }\n\n    public static final Parcelable.Creator<VariantInfo> CREATOR =\n        new Parcelable.Creator<VariantInfo>() {\n          @Override\n          public VariantInfo createFromParcel(Parcel in) {\n            return new VariantInfo(in);\n          }\n\n          @Override\n          public VariantInfo[] newArray(int size) {\n            return new VariantInfo[size];\n          }\n        };\n  }\n\n  /**\n   * The GROUP-ID value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the\n   * track is not derived from an EXT-X-MEDIA TAG.\n   */\n  @Nullable public final String groupId;\n  /**\n   * The NAME value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the\n   * track is not derived from an EXT-X-MEDIA TAG.\n   */\n  @Nullable public final String name;\n  /**\n   * The EXT-X-STREAM-INF tags attributes associated with this track. This field is non-applicable\n   * (and therefore empty) if this track is derived from an EXT-X-MEDIA tag.\n   */\n  public final List<VariantInfo> variantInfos;\n\n  /**\n   * Creates an instance.\n   *\n   * @param groupId See {@link #groupId}.\n   * @param name See {@link #name}.\n   * @param variantInfos See {@link #variantInfos}.\n   */\n  public HlsTrackMetadataEntry(\n      @Nullable String groupId, @Nullable String name, List<VariantInfo> variantInfos) {\n    this.groupId = groupId;\n    this.name = name;\n    this.variantInfos = Collections.unmodifiableList(new ArrayList<>(variantInfos));\n  }\n\n  /* package */ HlsTrackMetadataEntry(Parcel in) {\n    groupId = in.readString();\n    name = in.readString();\n    int variantInfoSize = in.readInt();\n    ArrayList<VariantInfo> variantInfos = new ArrayList<>(variantInfoSize);\n    for (int i = 0; i < variantInfoSize; i++) {\n      variantInfos.add(in.readParcelable(VariantInfo.class.getClassLoader()));\n    }\n    this.variantInfos = Collections.unmodifiableList(variantInfos);\n  }\n\n  @Override\n  public String toString() {\n    return \"HlsTrackMetadataEntry\" + (groupId != null ? (\" [\" + groupId + \", \" + name + \"]\") : \"\");\n  }\n\n  @Override\n  public boolean equals(@Nullable Object other) {\n    if (this == other) {\n      return true;\n    }\n    if (other == null || getClass() != other.getClass()) {\n      return false;\n    }\n\n    HlsTrackMetadataEntry that = (HlsTrackMetadataEntry) other;\n    return TextUtils.equals(groupId, that.groupId)\n        && TextUtils.equals(name, that.name)\n        && variantInfos.equals(that.variantInfos);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = groupId != null ? groupId.hashCode() : 0;\n    result = 31 * result + (name != null ? name.hashCode() : 0);\n    result = 31 * result + variantInfos.hashCode();\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(groupId);\n    dest.writeString(name);\n    int variantInfosSize = variantInfos.size();\n    dest.writeInt(variantInfosSize);\n    for (int i = 0; i < variantInfosSize; i++) {\n      dest.writeParcelable(variantInfos.get(i), /* parcelableFlags= */ 0);\n    }\n  }\n\n  public static final Parcelable.Creator<HlsTrackMetadataEntry> CREATOR =\n      new Parcelable.Creator<HlsTrackMetadataEntry>() {\n        @Override\n        public HlsTrackMetadataEntry createFromParcel(Parcel in) {\n          return new HlsTrackMetadataEntry(in);\n        }\n\n        @Override\n        public HlsTrackMetadataEntry[] newArray(int size) {\n          return new HlsTrackMetadataEntry[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.SampleQueue;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport java.io.IOException;\n\n/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */\npublic final class SampleQueueMappingException extends IOException {\n\n  /** @param mimeType The mime type of the track group whose mapping failed. */\n  public SampleQueueMappingException(@Nullable String mimeType) {\n    super(\"Unable to bind a sample queue to TrackGroup with mime type \" + mimeType + \".\");\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.util.SparseArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\n\n/**\n * Provides {@link TimestampAdjuster} instances for use during HLS playbacks.\n */\npublic final class TimestampAdjusterProvider {\n\n  // TODO: Prevent this array from growing indefinitely large by removing adjusters that are no\n  // longer required.\n  private final SparseArray<TimestampAdjuster> timestampAdjusters;\n\n  public TimestampAdjusterProvider() {\n    timestampAdjusters = new SparseArray<>();\n  }\n\n  /**\n   * Returns a {@link TimestampAdjuster} suitable for adjusting the pts timestamps contained in\n   * a chunk with a given discontinuity sequence.\n   *\n   * @param discontinuitySequence The chunk's discontinuity sequence.\n   * @return A {@link TimestampAdjuster}.\n   */\n  public TimestampAdjuster getAdjuster(int discontinuitySequence) {\n    TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);\n    if (adjuster == null) {\n      adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);\n      timestampAdjusters.put(discontinuitySequence, adjuster);\n    }\n    return adjuster;\n  }\n\n  /**\n   * Resets the provider.\n   */\n  public void reset() {\n    timestampAdjusters.clear();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls;\n\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * A special purpose extractor for WebVTT content in HLS.\n *\n * <p>This extractor passes through non-empty WebVTT files untouched, however derives the correct\n * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp\n * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to\n * derive a sample timestamp in this case.\n */\npublic final class WebvttExtractor implements Extractor {\n\n  private static final Pattern LOCAL_TIMESTAMP = Pattern.compile(\"LOCAL:([^,]+)\");\n  private static final Pattern MEDIA_TIMESTAMP = Pattern.compile(\"MPEGTS:(\\\\d+)\");\n  private static final int HEADER_MIN_LENGTH = 6 /* \"WEBVTT\" */;\n  private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH;\n\n  @Nullable private final String language;\n  private final TimestampAdjuster timestampAdjuster;\n  private final ParsableByteArray sampleDataWrapper;\n\n  private @MonotonicNonNull ExtractorOutput output;\n\n  private byte[] sampleData;\n  private int sampleSize;\n\n  public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) {\n    this.language = language;\n    this.timestampAdjuster = timestampAdjuster;\n    this.sampleDataWrapper = new ParsableByteArray();\n    sampleData = new byte[1024];\n  }\n\n  // Extractor implementation.\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    // Check whether there is a header without BOM.\n    input.peekFully(\n        sampleData, /* offset= */ 0, /* length= */ HEADER_MIN_LENGTH, /* allowEndOfInput= */ false);\n    sampleDataWrapper.reset(sampleData, HEADER_MIN_LENGTH);\n    if (WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper)) {\n      return true;\n    }\n    // The header did not match, try including the BOM.\n    input.peekFully(\n        sampleData,\n        /* offset= */ HEADER_MIN_LENGTH,\n        HEADER_MAX_LENGTH - HEADER_MIN_LENGTH,\n        /* allowEndOfInput= */ false);\n    sampleDataWrapper.reset(sampleData, HEADER_MAX_LENGTH);\n    return WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper);\n  }\n\n  @Override\n  public void init(ExtractorOutput output) {\n    this.output = output;\n    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    // This extractor is only used for the HLS use case, which should not call this method.\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  public int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    // output == null suggests init() hasn't been called\n    Assertions.checkNotNull(output);\n    int currentFileSize = (int) input.getLength();\n\n    // Increase the size of sampleData if necessary.\n    if (sampleSize == sampleData.length) {\n      sampleData = Arrays.copyOf(sampleData,\n          (currentFileSize != C.LENGTH_UNSET ? currentFileSize : sampleData.length) * 3 / 2);\n    }\n\n    // Consume to the input.\n    int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize);\n    if (bytesRead != C.RESULT_END_OF_INPUT) {\n      sampleSize += bytesRead;\n      if (currentFileSize == C.LENGTH_UNSET || sampleSize != currentFileSize) {\n        return Extractor.RESULT_CONTINUE;\n      }\n    }\n\n    // We've reached the end of the input, which corresponds to the end of the current file.\n    processSample();\n    return Extractor.RESULT_END_OF_INPUT;\n  }\n\n  @RequiresNonNull(\"output\")\n  private void processSample() throws ParserException {\n    ParsableByteArray webvttData = new ParsableByteArray(sampleData);\n\n    // Validate the first line of the header.\n    WebvttParserUtil.validateWebvttHeaderLine(webvttData);\n\n    // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header.\n    long vttTimestampUs = 0;\n    long tsTimestampUs = 0;\n\n    // Parse the remainder of the header looking for X-TIMESTAMP-MAP.\n    for (String line = webvttData.readLine();\n        !TextUtils.isEmpty(line);\n        line = webvttData.readLine()) {\n      if (line.startsWith(\"X-TIMESTAMP-MAP\")) {\n        Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line);\n        if (!localTimestampMatcher.find()) {\n          throw new ParserException(\"X-TIMESTAMP-MAP doesn't contain local timestamp: \" + line);\n        }\n        Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line);\n        if (!mediaTimestampMatcher.find()) {\n          throw new ParserException(\"X-TIMESTAMP-MAP doesn't contain media timestamp: \" + line);\n        }\n        vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1));\n        tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1)));\n      }\n    }\n\n    // Find the first cue header and parse the start time.\n    Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);\n    if (cueHeaderMatcher == null) {\n      // No cues found. Don't output a sample, but still output a corresponding track.\n      buildTrackOutput(0);\n      return;\n    }\n\n    long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));\n    long sampleTimeUs = timestampAdjuster.adjustTsTimestamp(\n        TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));\n    long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;\n    // Output the track.\n    TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);\n    // Output the sample.\n    sampleDataWrapper.reset(sampleData, sampleSize);\n    trackOutput.sampleData(sampleDataWrapper, sampleSize);\n    trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);\n  }\n\n  @RequiresNonNull(\"output\")\n  private TrackOutput buildTrackOutput(long subsampleOffsetUs) {\n    TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT);\n    trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null,\n        Format.NO_VALUE, 0, language, null, subsampleOffsetUs));\n    output.endTracks();\n    return trackOutput;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.offline;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.SegmentDownloader;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\n\n/**\n * A downloader for HLS streams.\n *\n * <p>Example usage:\n *\n * <pre>{@code\n * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);\n * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory(\"ExoPlayer\", null);\n * DownloaderConstructorHelper constructorHelper =\n *     new DownloaderConstructorHelper(cache, factory);\n * // Create a downloader for the first variant in a master playlist.\n * HlsDownloader hlsDownloader =\n *     new HlsDownloader(\n *         playlistUri,\n *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),\n *         constructorHelper);\n * // Perform the download.\n * hlsDownloader.download(progressListener);\n * // Access downloaded data using CacheDataSource\n * CacheDataSource cacheDataSource =\n *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);\n * }</pre>\n */\npublic final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {\n\n  /**\n   * @param playlistUri The {@link Uri} of the playlist to be downloaded.\n   * @param streamKeys Keys defining which renditions in the playlist should be selected for\n   *     download. If empty, all renditions are downloaded.\n   * @param constructorHelper A {@link DownloaderConstructorHelper} instance.\n   */\n  public HlsDownloader(\n      Uri playlistUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {\n    super(playlistUri, streamKeys, constructorHelper);\n  }\n\n  @Override\n  protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException {\n    return loadManifest(dataSource, dataSpec);\n  }\n\n  @Override\n  protected List<Segment> getSegments(\n      DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException {\n    ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>();\n    if (playlist instanceof HlsMasterPlaylist) {\n      HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;\n      addMediaPlaylistDataSpecs(masterPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs);\n    } else {\n      mediaPlaylistDataSpecs.add(\n          SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri)));\n    }\n\n    ArrayList<Segment> segments = new ArrayList<>();\n    HashSet<Uri> seenEncryptionKeyUris = new HashSet<>();\n    for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) {\n      segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec));\n      HlsMediaPlaylist mediaPlaylist;\n      try {\n        mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistDataSpec);\n      } catch (IOException e) {\n        if (!allowIncompleteList) {\n          throw e;\n        }\n        // Generating an incomplete segment list is allowed. Advance to the next media playlist.\n        continue;\n      }\n      HlsMediaPlaylist.Segment lastInitSegment = null;\n      List<HlsMediaPlaylist.Segment> hlsSegments = mediaPlaylist.segments;\n      for (int i = 0; i < hlsSegments.size(); i++) {\n        HlsMediaPlaylist.Segment segment = hlsSegments.get(i);\n        HlsMediaPlaylist.Segment initSegment = segment.initializationSegment;\n        if (initSegment != null && initSegment != lastInitSegment) {\n          lastInitSegment = initSegment;\n          addSegment(mediaPlaylist, initSegment, seenEncryptionKeyUris, segments);\n        }\n        addSegment(mediaPlaylist, segment, seenEncryptionKeyUris, segments);\n      }\n    }\n    return segments;\n  }\n\n  private void addMediaPlaylistDataSpecs(List<Uri> mediaPlaylistUrls, List<DataSpec> out) {\n    for (int i = 0; i < mediaPlaylistUrls.size(); i++) {\n      out.add(SegmentDownloader.getCompressibleDataSpec(mediaPlaylistUrls.get(i)));\n    }\n  }\n\n  private static HlsPlaylist loadManifest(DataSource dataSource, DataSpec dataSpec)\n      throws IOException {\n    return ParsingLoadable.load(\n        dataSource, new HlsPlaylistParser(), dataSpec, C.DATA_TYPE_MANIFEST);\n  }\n\n  private void addSegment(\n      HlsMediaPlaylist mediaPlaylist,\n      HlsMediaPlaylist.Segment segment,\n      HashSet<Uri> seenEncryptionKeyUris,\n      ArrayList<Segment> out) {\n    String baseUri = mediaPlaylist.baseUri;\n    long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;\n    if (segment.fullSegmentEncryptionKeyUri != null) {\n      Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri);\n      if (seenEncryptionKeyUris.add(keyUri)) {\n        out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri)));\n      }\n    }\n    Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url);\n    DataSpec dataSpec =\n        new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null);\n    out.add(new Segment(startTimeUs, dataSpec));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.hls.offline;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.hls;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\n\n/** Default implementation for {@link HlsPlaylistParserFactory}. */\npublic final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory {\n\n  @Override\n  public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {\n    return new HlsPlaylistParser();\n  }\n\n  @Override\n  public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(\n      HlsMasterPlaylist masterPlaylist) {\n    return new HlsPlaylistParser(masterPlaylist);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\n\n/** Default implementation for {@link HlsPlaylistTracker}. */\npublic final class DefaultHlsPlaylistTracker\n    implements HlsPlaylistTracker, Loader.Callback<ParsingLoadable<HlsPlaylist>> {\n\n  /** Factory for {@link DefaultHlsPlaylistTracker} instances. */\n  public static final Factory FACTORY = DefaultHlsPlaylistTracker::new;\n\n  /**\n   * Default coefficient applied on the target duration of a playlist to determine the amount of\n   * time after which an unchanging playlist is considered stuck.\n   */\n  public static final double DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;\n\n  private final HlsDataSourceFactory dataSourceFactory;\n  private final HlsPlaylistParserFactory playlistParserFactory;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final HashMap<Uri, MediaPlaylistBundle> playlistBundles;\n  private final List<PlaylistEventListener> listeners;\n  private final double playlistStuckTargetDurationCoefficient;\n\n  @Nullable private ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser;\n  @Nullable private EventDispatcher eventDispatcher;\n  @Nullable private Loader initialPlaylistLoader;\n  @Nullable private Handler playlistRefreshHandler;\n  @Nullable private PrimaryPlaylistListener primaryPlaylistListener;\n  @Nullable private HlsMasterPlaylist masterPlaylist;\n  @Nullable private Uri primaryMediaPlaylistUrl;\n  @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot;\n  private boolean isLive;\n  private long initialStartTimeUs;\n\n  /**\n   * Creates an instance.\n   *\n   * @param dataSourceFactory A factory for {@link DataSource} instances.\n   * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.\n   * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.\n   */\n  public DefaultHlsPlaylistTracker(\n      HlsDataSourceFactory dataSourceFactory,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      HlsPlaylistParserFactory playlistParserFactory) {\n    this(\n        dataSourceFactory,\n        loadErrorHandlingPolicy,\n        playlistParserFactory,\n        DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT);\n  }\n\n  /**\n   * Creates an instance.\n   *\n   * @param dataSourceFactory A factory for {@link DataSource} instances.\n   * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.\n   * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.\n   * @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of\n   *     media playlists in order to determine that a non-changing playlist is stuck. Once a\n   *     playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link\n   *     #maybeThrowPlaylistRefreshError(Uri)}.\n   */\n  public DefaultHlsPlaylistTracker(\n      HlsDataSourceFactory dataSourceFactory,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      HlsPlaylistParserFactory playlistParserFactory,\n      double playlistStuckTargetDurationCoefficient) {\n    this.dataSourceFactory = dataSourceFactory;\n    this.playlistParserFactory = playlistParserFactory;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient;\n    listeners = new ArrayList<>();\n    playlistBundles = new HashMap<>();\n    initialStartTimeUs = C.TIME_UNSET;\n  }\n\n  // HlsPlaylistTracker implementation.\n\n  @Override\n  public void start(\n      Uri initialPlaylistUri,\n      EventDispatcher eventDispatcher,\n      PrimaryPlaylistListener primaryPlaylistListener) {\n    this.playlistRefreshHandler = new Handler();\n    this.eventDispatcher = eventDispatcher;\n    this.primaryPlaylistListener = primaryPlaylistListener;\n    ParsingLoadable<HlsPlaylist> masterPlaylistLoadable =\n        new ParsingLoadable<>(\n            dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),\n            initialPlaylistUri,\n            C.DATA_TYPE_MANIFEST,\n            playlistParserFactory.createPlaylistParser());\n    Assertions.checkState(initialPlaylistLoader == null);\n    initialPlaylistLoader = new Loader(\"DefaultHlsPlaylistTracker:MasterPlaylist\");\n    long elapsedRealtime =\n        initialPlaylistLoader.startLoading(\n            masterPlaylistLoadable,\n            this,\n            loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type));\n    eventDispatcher.loadStarted(\n        masterPlaylistLoadable.dataSpec,\n        masterPlaylistLoadable.type,\n        elapsedRealtime);\n  }\n\n  @Override\n  public void stop() {\n    primaryMediaPlaylistUrl = null;\n    primaryMediaPlaylistSnapshot = null;\n    masterPlaylist = null;\n    initialStartTimeUs = C.TIME_UNSET;\n    initialPlaylistLoader.release();\n    initialPlaylistLoader = null;\n    for (MediaPlaylistBundle bundle : playlistBundles.values()) {\n      bundle.release();\n    }\n    playlistRefreshHandler.removeCallbacksAndMessages(null);\n    playlistRefreshHandler = null;\n    playlistBundles.clear();\n  }\n\n  @Override\n  public void addListener(PlaylistEventListener listener) {\n    listeners.add(listener);\n  }\n\n  @Override\n  public void removeListener(PlaylistEventListener listener) {\n    listeners.remove(listener);\n  }\n\n  @Override\n  @Nullable\n  public HlsMasterPlaylist getMasterPlaylist() {\n    return masterPlaylist;\n  }\n\n  @Override\n  @Nullable\n  public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) {\n    HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();\n    if (snapshot != null && isForPlayback) {\n      maybeSetPrimaryUrl(url);\n    }\n    return snapshot;\n  }\n\n  @Override\n  public long getInitialStartTimeUs() {\n    return initialStartTimeUs;\n  }\n\n  @Override\n  public boolean isSnapshotValid(Uri url) {\n    return playlistBundles.get(url).isSnapshotValid();\n  }\n\n  @Override\n  public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {\n    if (initialPlaylistLoader != null) {\n      initialPlaylistLoader.maybeThrowError();\n    }\n    if (primaryMediaPlaylistUrl != null) {\n      maybeThrowPlaylistRefreshError(primaryMediaPlaylistUrl);\n    }\n  }\n\n  @Override\n  public void maybeThrowPlaylistRefreshError(Uri url) throws IOException {\n    playlistBundles.get(url).maybeThrowPlaylistRefreshError();\n  }\n\n  @Override\n  public void refreshPlaylist(Uri url) {\n    playlistBundles.get(url).loadPlaylist();\n  }\n\n  @Override\n  public boolean isLive() {\n    return isLive;\n  }\n\n  // Loader.Callback implementation.\n\n  @Override\n  public void onLoadCompleted(\n      ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {\n    HlsPlaylist result = loadable.getResult();\n    HlsMasterPlaylist masterPlaylist;\n    boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;\n    if (isMediaPlaylist) {\n      masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);\n    } else /* result instanceof HlsMasterPlaylist */ {\n      masterPlaylist = (HlsMasterPlaylist) result;\n    }\n    this.masterPlaylist = masterPlaylist;\n    mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist);\n    primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url;\n    createBundles(masterPlaylist.mediaPlaylistUrls);\n    MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);\n    if (isMediaPlaylist) {\n      // We don't need to load the playlist again. We can use the same result.\n      primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs);\n    } else {\n      primaryBundle.loadPlaylist();\n    }\n    eventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        C.DATA_TYPE_MANIFEST,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n  }\n\n  @Override\n  public void onLoadCanceled(\n      ParsingLoadable<HlsPlaylist> loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      boolean released) {\n    eventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        C.DATA_TYPE_MANIFEST,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      ParsingLoadable<HlsPlaylist> loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long retryDelayMs =\n        loadErrorHandlingPolicy.getRetryDelayMsFor(\n            loadable.type, loadDurationMs, error, errorCount);\n    boolean isFatal = retryDelayMs == C.TIME_UNSET;\n    eventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        C.DATA_TYPE_MANIFEST,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded(),\n        error,\n        isFatal);\n    return isFatal\n        ? Loader.DONT_RETRY_FATAL\n        : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);\n  }\n\n  // Internal methods.\n\n  private boolean maybeSelectNewPrimaryUrl() {\n    List<Variant> variants = masterPlaylist.variants;\n    int variantsSize = variants.size();\n    long currentTimeMs = SystemClock.elapsedRealtime();\n    for (int i = 0; i < variantsSize; i++) {\n      MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url);\n      if (currentTimeMs > bundle.blacklistUntilMs) {\n        primaryMediaPlaylistUrl = bundle.playlistUrl;\n        bundle.loadPlaylist();\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private void maybeSetPrimaryUrl(Uri url) {\n    if (url.equals(primaryMediaPlaylistUrl)\n        || !isVariantUrl(url)\n        || (primaryMediaPlaylistSnapshot != null && primaryMediaPlaylistSnapshot.hasEndTag)) {\n      // Ignore if the primary media playlist URL is unchanged, if the media playlist is not\n      // referenced directly by a variant, or it the last primary snapshot contains an end tag.\n      return;\n    }\n    primaryMediaPlaylistUrl = url;\n    playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist();\n  }\n\n  /** Returns whether any of the variants in the master playlist have the specified playlist URL. */\n  private boolean isVariantUrl(Uri playlistUrl) {\n    List<Variant> variants = masterPlaylist.variants;\n    for (int i = 0; i < variants.size(); i++) {\n      if (playlistUrl.equals(variants.get(i).url)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private void createBundles(List<Uri> urls) {\n    int listSize = urls.size();\n    for (int i = 0; i < listSize; i++) {\n      Uri url = urls.get(i);\n      MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);\n      playlistBundles.put(url, bundle);\n    }\n  }\n\n  /**\n   * Called by the bundles when a snapshot changes.\n   *\n   * @param url The url of the playlist.\n   * @param newSnapshot The new snapshot.\n   */\n  private void onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot) {\n    if (url.equals(primaryMediaPlaylistUrl)) {\n      if (primaryMediaPlaylistSnapshot == null) {\n        // This is the first primary url snapshot.\n        isLive = !newSnapshot.hasEndTag;\n        initialStartTimeUs = newSnapshot.startTimeUs;\n      }\n      primaryMediaPlaylistSnapshot = newSnapshot;\n      primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);\n    }\n    int listenersSize = listeners.size();\n    for (int i = 0; i < listenersSize; i++) {\n      listeners.get(i).onPlaylistChanged();\n    }\n  }\n\n  private boolean notifyPlaylistError(Uri playlistUrl, long blacklistDurationMs) {\n    int listenersSize = listeners.size();\n    boolean anyBlacklistingFailed = false;\n    for (int i = 0; i < listenersSize; i++) {\n      anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, blacklistDurationMs);\n    }\n    return anyBlacklistingFailed;\n  }\n\n  private HlsMediaPlaylist getLatestPlaylistSnapshot(\n      HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {\n    if (!loadedPlaylist.isNewerThan(oldPlaylist)) {\n      if (loadedPlaylist.hasEndTag) {\n        // If the loaded playlist has an end tag but is not newer than the old playlist then we have\n        // an inconsistent state. This is typically caused by the server incorrectly resetting the\n        // media sequence when appending the end tag. We resolve this case as best we can by\n        // returning the old playlist with the end tag appended.\n        return oldPlaylist.copyWithEndTag();\n      } else {\n        return oldPlaylist;\n      }\n    }\n    long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);\n    int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);\n    return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);\n  }\n\n  private long getLoadedPlaylistStartTimeUs(\n      HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {\n    if (loadedPlaylist.hasProgramDateTime) {\n      return loadedPlaylist.startTimeUs;\n    }\n    long primarySnapshotStartTimeUs =\n        primaryMediaPlaylistSnapshot != null ? primaryMediaPlaylistSnapshot.startTimeUs : 0;\n    if (oldPlaylist == null) {\n      return primarySnapshotStartTimeUs;\n    }\n    int oldPlaylistSize = oldPlaylist.segments.size();\n    Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);\n    if (firstOldOverlappingSegment != null) {\n      return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;\n    } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {\n      return oldPlaylist.getEndTimeUs();\n    } else {\n      // No segments overlap, we assume the new playlist start coincides with the primary playlist.\n      return primarySnapshotStartTimeUs;\n    }\n  }\n\n  private int getLoadedPlaylistDiscontinuitySequence(\n      HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {\n    if (loadedPlaylist.hasDiscontinuitySequence) {\n      return loadedPlaylist.discontinuitySequence;\n    }\n    // TODO: Improve cross-playlist discontinuity adjustment.\n    int primaryUrlDiscontinuitySequence =\n        primaryMediaPlaylistSnapshot != null\n            ? primaryMediaPlaylistSnapshot.discontinuitySequence\n            : 0;\n    if (oldPlaylist == null) {\n      return primaryUrlDiscontinuitySequence;\n    }\n    Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);\n    if (firstOldOverlappingSegment != null) {\n      return oldPlaylist.discontinuitySequence\n          + firstOldOverlappingSegment.relativeDiscontinuitySequence\n          - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;\n    }\n    return primaryUrlDiscontinuitySequence;\n  }\n\n  private static Segment getFirstOldOverlappingSegment(\n      HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {\n    int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence);\n    List<Segment> oldSegments = oldPlaylist.segments;\n    return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;\n  }\n\n  /** Holds all information related to a specific Media Playlist. */\n  private final class MediaPlaylistBundle\n      implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable {\n\n    private final Uri playlistUrl;\n    private final Loader mediaPlaylistLoader;\n    private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;\n\n    @Nullable private HlsMediaPlaylist playlistSnapshot;\n    private long lastSnapshotLoadMs;\n    private long lastSnapshotChangeMs;\n    private long earliestNextLoadTimeMs;\n    private long blacklistUntilMs;\n    private boolean loadPending;\n    private IOException playlistError;\n\n    public MediaPlaylistBundle(Uri playlistUrl) {\n      this.playlistUrl = playlistUrl;\n      mediaPlaylistLoader = new Loader(\"DefaultHlsPlaylistTracker:MediaPlaylist\");\n      mediaPlaylistLoadable =\n          new ParsingLoadable<>(\n              dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),\n              playlistUrl,\n              C.DATA_TYPE_MANIFEST,\n              mediaPlaylistParser);\n    }\n\n    @Nullable\n    public HlsMediaPlaylist getPlaylistSnapshot() {\n      return playlistSnapshot;\n    }\n\n    public boolean isSnapshotValid() {\n      if (playlistSnapshot == null) {\n        return false;\n      }\n      long currentTimeMs = SystemClock.elapsedRealtime();\n      long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));\n      return playlistSnapshot.hasEndTag\n          || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT\n          || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD\n          || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;\n    }\n\n    public void release() {\n      mediaPlaylistLoader.release();\n    }\n\n    public void loadPlaylist() {\n      blacklistUntilMs = 0;\n      if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) {\n        // Load already pending, in progress, or a fatal error has been encountered. Do nothing.\n        return;\n      }\n      long currentTimeMs = SystemClock.elapsedRealtime();\n      if (currentTimeMs < earliestNextLoadTimeMs) {\n        loadPending = true;\n        playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);\n      } else {\n        loadPlaylistImmediately();\n      }\n    }\n\n    public void maybeThrowPlaylistRefreshError() throws IOException {\n      mediaPlaylistLoader.maybeThrowError();\n      if (playlistError != null) {\n        throw playlistError;\n      }\n    }\n\n    // Loader.Callback implementation.\n\n    @Override\n    public void onLoadCompleted(\n        ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {\n      HlsPlaylist result = loadable.getResult();\n      if (result instanceof HlsMediaPlaylist) {\n        processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs);\n        eventDispatcher.loadCompleted(\n            loadable.dataSpec,\n            loadable.getUri(),\n            loadable.getResponseHeaders(),\n            C.DATA_TYPE_MANIFEST,\n            elapsedRealtimeMs,\n            loadDurationMs,\n            loadable.bytesLoaded());\n      } else {\n        playlistError = new ParserException(\"Loaded playlist has unexpected type.\");\n      }\n    }\n\n    @Override\n    public void onLoadCanceled(\n        ParsingLoadable<HlsPlaylist> loadable,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        boolean released) {\n      eventDispatcher.loadCanceled(\n          loadable.dataSpec,\n          loadable.getUri(),\n          loadable.getResponseHeaders(),\n          C.DATA_TYPE_MANIFEST,\n          elapsedRealtimeMs,\n          loadDurationMs,\n          loadable.bytesLoaded());\n    }\n\n    @Override\n    public LoadErrorAction onLoadError(\n        ParsingLoadable<HlsPlaylist> loadable,\n        long elapsedRealtimeMs,\n        long loadDurationMs,\n        IOException error,\n        int errorCount) {\n      LoadErrorAction loadErrorAction;\n\n      long blacklistDurationMs =\n          loadErrorHandlingPolicy.getBlacklistDurationMsFor(\n              loadable.type, loadDurationMs, error, errorCount);\n      boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET;\n\n      boolean blacklistingFailed =\n          notifyPlaylistError(playlistUrl, blacklistDurationMs) || !shouldBlacklist;\n      if (shouldBlacklist) {\n        blacklistingFailed |= blacklistPlaylist(blacklistDurationMs);\n      }\n\n      if (blacklistingFailed) {\n        long retryDelay =\n            loadErrorHandlingPolicy.getRetryDelayMsFor(\n                loadable.type, loadDurationMs, error, errorCount);\n        loadErrorAction =\n            retryDelay != C.TIME_UNSET\n                ? Loader.createRetryAction(false, retryDelay)\n                : Loader.DONT_RETRY_FATAL;\n      } else {\n        loadErrorAction = Loader.DONT_RETRY;\n      }\n\n      eventDispatcher.loadError(\n          loadable.dataSpec,\n          loadable.getUri(),\n          loadable.getResponseHeaders(),\n          C.DATA_TYPE_MANIFEST,\n          elapsedRealtimeMs,\n          loadDurationMs,\n          loadable.bytesLoaded(),\n          error,\n          /* wasCanceled= */ !loadErrorAction.isRetry());\n\n      return loadErrorAction;\n    }\n\n    // Runnable implementation.\n\n    @Override\n    public void run() {\n      loadPending = false;\n      loadPlaylistImmediately();\n    }\n\n    // Internal methods.\n\n    private void loadPlaylistImmediately() {\n      long elapsedRealtime =\n          mediaPlaylistLoader.startLoading(\n              mediaPlaylistLoadable,\n              this,\n              loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type));\n      eventDispatcher.loadStarted(\n          mediaPlaylistLoadable.dataSpec,\n          mediaPlaylistLoadable.type,\n          elapsedRealtime);\n    }\n\n    private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) {\n      HlsMediaPlaylist oldPlaylist = playlistSnapshot;\n      long currentTimeMs = SystemClock.elapsedRealtime();\n      lastSnapshotLoadMs = currentTimeMs;\n      playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);\n      if (playlistSnapshot != oldPlaylist) {\n        playlistError = null;\n        lastSnapshotChangeMs = currentTimeMs;\n        onPlaylistUpdated(playlistUrl, playlistSnapshot);\n      } else if (!playlistSnapshot.hasEndTag) {\n        if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()\n            < playlistSnapshot.mediaSequence) {\n          // TODO: Allow customization of playlist resets handling.\n          // The media sequence jumped backwards. The server has probably reset. We do not try\n          // blacklisting in this case.\n          playlistError = new PlaylistResetException(playlistUrl);\n          notifyPlaylistError(playlistUrl, C.TIME_UNSET);\n        } else if (currentTimeMs - lastSnapshotChangeMs\n            > C.usToMs(playlistSnapshot.targetDurationUs)\n                * playlistStuckTargetDurationCoefficient) {\n          // TODO: Allow customization of stuck playlists handling.\n          playlistError = new PlaylistStuckException(playlistUrl);\n          long blacklistDurationMs =\n              loadErrorHandlingPolicy.getBlacklistDurationMsFor(\n                  C.DATA_TYPE_MANIFEST, loadDurationMs, playlistError, /* errorCount= */ 1);\n          notifyPlaylistError(playlistUrl, blacklistDurationMs);\n          if (blacklistDurationMs != C.TIME_UNSET) {\n            blacklistPlaylist(blacklistDurationMs);\n          }\n        }\n      }\n      // Do not allow the playlist to load again within the target duration if we obtained a new\n      // snapshot, or half the target duration otherwise.\n      earliestNextLoadTimeMs =\n          currentTimeMs\n              + C.usToMs(\n                  playlistSnapshot != oldPlaylist\n                      ? playlistSnapshot.targetDurationUs\n                      : (playlistSnapshot.targetDurationUs / 2));\n      // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the\n      // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes\n      // the primary.\n      if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {\n        loadPlaylist();\n      }\n    }\n\n    /**\n     * Blacklists the playlist.\n     *\n     * @param blacklistDurationMs The number of milliseconds for which the playlist should be\n     *     blacklisted.\n     * @return Whether the playlist is the primary, despite being blacklisted.\n     */\n    private boolean blacklistPlaylist(long blacklistDurationMs) {\n      blacklistUntilMs = SystemClock.elapsedRealtime() + blacklistDurationMs;\n      return playlistUrl.equals(primaryMediaPlaylistUrl) && !maybeSelectNewPrimaryUrl();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport com.google.android.exoplayer2.offline.FilteringManifestParser;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport java.util.List;\n\n/**\n * A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream\n * keys.\n */\npublic final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {\n\n  private final HlsPlaylistParserFactory hlsPlaylistParserFactory;\n  private final List<StreamKey> streamKeys;\n\n  /**\n   * @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be\n   *     filtered.\n   * @param streamKeys The stream keys. If null or empty then filtering will not occur.\n   */\n  public FilteringHlsPlaylistParserFactory(\n      HlsPlaylistParserFactory hlsPlaylistParserFactory, List<StreamKey> streamKeys) {\n    this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;\n    this.streamKeys = streamKeys;\n  }\n\n  @Override\n  public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {\n    return new FilteringManifestParser<>(\n        hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);\n  }\n\n  @Override\n  public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(\n      HlsMasterPlaylist masterPlaylist) {\n    return new FilteringManifestParser<>(\n        hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/** Represents an HLS master playlist. */\npublic final class HlsMasterPlaylist extends HlsPlaylist {\n\n  /** Represents an empty master playlist, from which no attributes can be inherited. */\n  public static final HlsMasterPlaylist EMPTY =\n      new HlsMasterPlaylist(\n          /* baseUri= */ \"\",\n          /* tags= */ Collections.emptyList(),\n          /* variants= */ Collections.emptyList(),\n          /* videos= */ Collections.emptyList(),\n          /* audios= */ Collections.emptyList(),\n          /* subtitles= */ Collections.emptyList(),\n          /* closedCaptions= */ Collections.emptyList(),\n          /* muxedAudioFormat= */ null,\n          /* muxedCaptionFormats= */ Collections.emptyList(),\n          /* hasIndependentSegments= */ false,\n          /* variableDefinitions= */ Collections.emptyMap(),\n          /* sessionKeyDrmInitData= */ Collections.emptyList());\n\n  // These constants must not be changed because they are persisted in offline stream keys.\n  public static final int GROUP_INDEX_VARIANT = 0;\n  public static final int GROUP_INDEX_AUDIO = 1;\n  public static final int GROUP_INDEX_SUBTITLE = 2;\n\n  /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */\n  public static final class Variant {\n\n    /** The variant's url. */\n    public final Uri url;\n\n    /** Format information associated with this variant. */\n    public final Format format;\n\n    /** The video rendition group referenced by this variant, or {@code null}. */\n    @Nullable public final String videoGroupId;\n\n    /** The audio rendition group referenced by this variant, or {@code null}. */\n    @Nullable public final String audioGroupId;\n\n    /** The subtitle rendition group referenced by this variant, or {@code null}. */\n    @Nullable public final String subtitleGroupId;\n\n    /** The caption rendition group referenced by this variant, or {@code null}. */\n    @Nullable public final String captionGroupId;\n\n    /**\n     * @param url See {@link #url}.\n     * @param format See {@link #format}.\n     * @param videoGroupId See {@link #videoGroupId}.\n     * @param audioGroupId See {@link #audioGroupId}.\n     * @param subtitleGroupId See {@link #subtitleGroupId}.\n     * @param captionGroupId See {@link #captionGroupId}.\n     */\n    public Variant(\n        Uri url,\n        Format format,\n        @Nullable String videoGroupId,\n        @Nullable String audioGroupId,\n        @Nullable String subtitleGroupId,\n        @Nullable String captionGroupId) {\n      this.url = url;\n      this.format = format;\n      this.videoGroupId = videoGroupId;\n      this.audioGroupId = audioGroupId;\n      this.subtitleGroupId = subtitleGroupId;\n      this.captionGroupId = captionGroupId;\n    }\n\n    /**\n     * Creates a variant for a given media playlist url.\n     *\n     * @param url The media playlist url.\n     * @return The variant instance.\n     */\n    public static Variant createMediaPlaylistVariantUrl(Uri url) {\n      Format format =\n          Format.createContainerFormat(\n              \"0\",\n              /* label= */ null,\n              MimeTypes.APPLICATION_M3U8,\n              /* sampleMimeType= */ null,\n              /* codecs= */ null,\n              /* bitrate= */ Format.NO_VALUE,\n              /* selectionFlags= */ 0,\n              /* roleFlags= */ 0,\n              /* language= */ null);\n      return new Variant(\n          url,\n          format,\n          /* videoGroupId= */ null,\n          /* audioGroupId= */ null,\n          /* subtitleGroupId= */ null,\n          /* captionGroupId= */ null);\n    }\n\n    /** Returns a copy of this instance with the given {@link Format}. */\n    public Variant copyWithFormat(Format format) {\n      return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId);\n    }\n  }\n\n  /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */\n  public static final class Rendition {\n\n    /** The rendition's url, or null if the tag does not have a URI attribute. */\n    @Nullable public final Uri url;\n\n    /** Format information associated with this rendition. */\n    public final Format format;\n\n    /** The group to which this rendition belongs. */\n    public final String groupId;\n\n    /** The name of the rendition. */\n    public final String name;\n\n    /**\n     * @param url See {@link #url}.\n     * @param format See {@link #format}.\n     * @param groupId See {@link #groupId}.\n     * @param name See {@link #name}.\n     */\n    public Rendition(@Nullable Uri url, Format format, String groupId, String name) {\n      this.url = url;\n      this.format = format;\n      this.groupId = groupId;\n      this.name = name;\n    }\n\n  }\n\n  /** All of the media playlist URLs referenced by the playlist. */\n  public final List<Uri> mediaPlaylistUrls;\n  /** The variants declared by the playlist. */\n  public final List<Variant> variants;\n  /** The video renditions declared by the playlist. */\n  public final List<Rendition> videos;\n  /** The audio renditions declared by the playlist. */\n  public final List<Rendition> audios;\n  /** The subtitle renditions declared by the playlist. */\n  public final List<Rendition> subtitles;\n  /** The closed caption renditions declared by the playlist. */\n  public final List<Rendition> closedCaptions;\n\n  /**\n   * The format of the audio muxed in the variants. May be null if the playlist does not declare any\n   * muxed audio.\n   */\n  @Nullable public final Format muxedAudioFormat;\n  /**\n   * The format of the closed captions declared by the playlist. May be empty if the playlist\n   * explicitly declares no captions are available, or null if the playlist does not declare any\n   * captions information.\n   */\n  @Nullable public final List<Format> muxedCaptionFormats;\n  /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */\n  public final Map<String, String> variableDefinitions;\n  /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */\n  public final List<DrmInitData> sessionKeyDrmInitData;\n\n  /**\n   * @param baseUri See {@link #baseUri}.\n   * @param tags See {@link #tags}.\n   * @param variants See {@link #variants}.\n   * @param videos See {@link #videos}.\n   * @param audios See {@link #audios}.\n   * @param subtitles See {@link #subtitles}.\n   * @param closedCaptions See {@link #closedCaptions}.\n   * @param muxedAudioFormat See {@link #muxedAudioFormat}.\n   * @param muxedCaptionFormats See {@link #muxedCaptionFormats}.\n   * @param hasIndependentSegments See {@link #hasIndependentSegments}.\n   * @param variableDefinitions See {@link #variableDefinitions}.\n   * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.\n   */\n  public HlsMasterPlaylist(\n      String baseUri,\n      List<String> tags,\n      List<Variant> variants,\n      List<Rendition> videos,\n      List<Rendition> audios,\n      List<Rendition> subtitles,\n      List<Rendition> closedCaptions,\n      @Nullable Format muxedAudioFormat,\n      @Nullable List<Format> muxedCaptionFormats,\n      boolean hasIndependentSegments,\n      Map<String, String> variableDefinitions,\n      List<DrmInitData> sessionKeyDrmInitData) {\n    super(baseUri, tags, hasIndependentSegments);\n    this.mediaPlaylistUrls =\n        Collections.unmodifiableList(\n            getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));\n    this.variants = Collections.unmodifiableList(variants);\n    this.videos = Collections.unmodifiableList(videos);\n    this.audios = Collections.unmodifiableList(audios);\n    this.subtitles = Collections.unmodifiableList(subtitles);\n    this.closedCaptions = Collections.unmodifiableList(closedCaptions);\n    this.muxedAudioFormat = muxedAudioFormat;\n    this.muxedCaptionFormats = muxedCaptionFormats != null\n        ? Collections.unmodifiableList(muxedCaptionFormats) : null;\n    this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);\n    this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);\n  }\n\n  @Override\n  public HlsMasterPlaylist copy(List<StreamKey> streamKeys) {\n    return new HlsMasterPlaylist(\n        baseUri,\n        tags,\n        copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys),\n        // TODO: Allow stream keys to specify video renditions to be retained.\n        /* videos= */ Collections.emptyList(),\n        copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys),\n        copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),\n        // TODO: Update to retain all closed captions.\n        /* closedCaptions= */ Collections.emptyList(),\n        muxedAudioFormat,\n        muxedCaptionFormats,\n        hasIndependentSegments,\n        variableDefinitions,\n        sessionKeyDrmInitData);\n  }\n\n  /**\n   * Creates a playlist with a single variant.\n   *\n   * @param variantUrl The url of the single variant.\n   * @return A master playlist with a single variant for the provided url.\n   */\n  public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {\n    List<Variant> variant =\n        Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));\n    return new HlsMasterPlaylist(\n        /* baseUri= */ \"\",\n        /* tags= */ Collections.emptyList(),\n        variant,\n        /* videos= */ Collections.emptyList(),\n        /* audios= */ Collections.emptyList(),\n        /* subtitles= */ Collections.emptyList(),\n        /* closedCaptions= */ Collections.emptyList(),\n        /* muxedAudioFormat= */ null,\n        /* muxedCaptionFormats= */ null,\n        /* hasIndependentSegments= */ false,\n        /* variableDefinitions= */ Collections.emptyMap(),\n        /* sessionKeyDrmInitData= */ Collections.emptyList());\n  }\n\n  private static List<Uri> getMediaPlaylistUrls(\n      List<Variant> variants,\n      List<Rendition> videos,\n      List<Rendition> audios,\n      List<Rendition> subtitles,\n      List<Rendition> closedCaptions) {\n    ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();\n    for (int i = 0; i < variants.size(); i++) {\n      Uri uri = variants.get(i).url;\n      if (!mediaPlaylistUrls.contains(uri)) {\n        mediaPlaylistUrls.add(uri);\n      }\n    }\n    addMediaPlaylistUrls(videos, mediaPlaylistUrls);\n    addMediaPlaylistUrls(audios, mediaPlaylistUrls);\n    addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);\n    addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);\n    return mediaPlaylistUrls;\n  }\n\n  private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {\n    for (int i = 0; i < renditions.size(); i++) {\n      Uri uri = renditions.get(i).url;\n      if (uri != null && !out.contains(uri)) {\n        out.add(uri);\n      }\n    }\n  }\n\n  private static <T> List<T> copyStreams(\n      List<T> streams, int groupIndex, List<StreamKey> streamKeys) {\n    List<T> copiedStreams = new ArrayList<>(streamKeys.size());\n    // TODO:\n    // 1. When variants with the same URL are not de-duplicated, duplicates must not increment\n    //    trackIndex so as to avoid breaking stream keys that have been persisted for offline. All\n    //    duplicates should be copied if the first variant is copied, or discarded otherwise.\n    // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to\n    //    avoid breaking stream keys that have been persisted for offline. All renitions with null\n    //    URLs should be copied. They may become unreachable if all variants that reference them are\n    //    removed, but this is OK.\n    // 3. Renditions with URLs matching copied variants should always themselves be copied, even if\n    //    the corresponding stream key is omitted. Else we're throwing away information for no gain.\n    for (int i = 0; i < streams.size(); i++) {\n      T stream = streams.get(i);\n      for (int j = 0; j < streamKeys.size(); j++) {\n        StreamKey streamKey = streamKeys.get(j);\n        if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) {\n          copiedStreams.add(stream);\n          break;\n        }\n      }\n    }\n    return copiedStreams;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Represents an HLS media playlist. */\npublic final class HlsMediaPlaylist extends HlsPlaylist {\n\n  /** Media segment reference. */\n  @SuppressWarnings(\"ComparableType\")\n  public static final class Segment implements Comparable<Long> {\n\n    /**\n     * The url of the segment.\n     */\n    public final String url;\n    /**\n     * The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if\n     * the media playlist does not define a media section for this segment. The same instance is\n     * used for all segments that share an EXT-X-MAP tag.\n     */\n    @Nullable public final Segment initializationSegment;\n    /** The duration of the segment in microseconds, as defined by #EXTINF. */\n    public final long durationUs;\n    /** The human readable title of the segment. */\n    public final String title;\n    /**\n     * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment.\n     */\n    public final int relativeDiscontinuitySequence;\n    /**\n     * The start time of the segment in microseconds, relative to the start of the playlist.\n     */\n    public final long relativeStartTimeUs;\n    /**\n     * DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM\n     * protection.\n     */\n    @Nullable public final DrmInitData drmInitData;\n    /**\n     * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use\n     * full segment encryption with identity key.\n     */\n    @Nullable public final String fullSegmentEncryptionKeyUri;\n    /**\n     * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not\n     * encrypted.\n     */\n    @Nullable public final String encryptionIV;\n    /**\n     * The segment's byte range offset, as defined by #EXT-X-BYTERANGE.\n     */\n    public final long byterangeOffset;\n    /**\n     * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if\n     * no byte range is specified.\n     */\n    public final long byterangeLength;\n\n    /** Whether the segment is tagged with #EXT-X-GAP. */\n    public final boolean hasGapTag;\n\n    /**\n     * @param uri See {@link #url}.\n     * @param byterangeOffset See {@link #byterangeOffset}.\n     * @param byterangeLength See {@link #byterangeLength}.\n     * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.\n     * @param encryptionIV See {@link #encryptionIV}.\n     */\n    public Segment(\n        String uri,\n        long byterangeOffset,\n        long byterangeLength,\n        @Nullable String fullSegmentEncryptionKeyUri,\n        @Nullable String encryptionIV) {\n      this(\n          uri,\n          /* initializationSegment= */ null,\n          /* title= */ \"\",\n          /* durationUs= */ 0,\n          /* relativeDiscontinuitySequence= */ -1,\n          /* relativeStartTimeUs= */ C.TIME_UNSET,\n          /* drmInitData= */ null,\n          fullSegmentEncryptionKeyUri,\n          encryptionIV,\n          byterangeOffset,\n          byterangeLength,\n          /* hasGapTag= */ false);\n    }\n\n    /**\n     * @param url See {@link #url}.\n     * @param initializationSegment See {@link #initializationSegment}.\n     * @param title See {@link #title}.\n     * @param durationUs See {@link #durationUs}.\n     * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.\n     * @param relativeStartTimeUs See {@link #relativeStartTimeUs}.\n     * @param drmInitData See {@link #drmInitData}.\n     * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.\n     * @param encryptionIV See {@link #encryptionIV}.\n     * @param byterangeOffset See {@link #byterangeOffset}.\n     * @param byterangeLength See {@link #byterangeLength}.\n     * @param hasGapTag See {@link #hasGapTag}.\n     */\n    public Segment(\n        String url,\n        @Nullable Segment initializationSegment,\n        String title,\n        long durationUs,\n        int relativeDiscontinuitySequence,\n        long relativeStartTimeUs,\n        @Nullable DrmInitData drmInitData,\n        @Nullable String fullSegmentEncryptionKeyUri,\n        @Nullable String encryptionIV,\n        long byterangeOffset,\n        long byterangeLength,\n        boolean hasGapTag) {\n      this.url = url;\n      this.initializationSegment = initializationSegment;\n      this.title = title;\n      this.durationUs = durationUs;\n      this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;\n      this.relativeStartTimeUs = relativeStartTimeUs;\n      this.drmInitData = drmInitData;\n      this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;\n      this.encryptionIV = encryptionIV;\n      this.byterangeOffset = byterangeOffset;\n      this.byterangeLength = byterangeLength;\n      this.hasGapTag = hasGapTag;\n    }\n\n    @Override\n    public int compareTo(Long relativeStartTimeUs) {\n      return this.relativeStartTimeUs > relativeStartTimeUs\n          ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);\n    }\n\n  }\n\n  /**\n   * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link\n   * #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT})\n  public @interface PlaylistType {}\n\n  public static final int PLAYLIST_TYPE_UNKNOWN = 0;\n  public static final int PLAYLIST_TYPE_VOD = 1;\n  public static final int PLAYLIST_TYPE_EVENT = 2;\n\n  /**\n   * The type of the playlist. See {@link PlaylistType}.\n   */\n  @PlaylistType public final int playlistType;\n  /**\n   * The start offset in microseconds, as defined by #EXT-X-START.\n   */\n  public final long startOffsetUs;\n  /**\n   * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.\n   * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the\n   * playlist.\n   */\n  public final long startTimeUs;\n  /**\n   * Whether the playlist contains the #EXT-X-DISCONTINUITY-SEQUENCE tag.\n   */\n  public final boolean hasDiscontinuitySequence;\n  /**\n   * The discontinuity sequence number of the first media segment in the playlist, as defined by\n   * #EXT-X-DISCONTINUITY-SEQUENCE.\n   */\n  public final int discontinuitySequence;\n  /**\n   * The media sequence number of the first media segment in the playlist, as defined by\n   * #EXT-X-MEDIA-SEQUENCE.\n   */\n  public final long mediaSequence;\n  /**\n   * The compatibility version, as defined by #EXT-X-VERSION.\n   */\n  public final int version;\n  /**\n   * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.\n   */\n  public final long targetDurationUs;\n  /**\n   * Whether the playlist contains the #EXT-X-ENDLIST tag.\n   */\n  public final boolean hasEndTag;\n  /**\n   * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag.\n   */\n  public final boolean hasProgramDateTime;\n  /**\n   * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key\n   * acquisition data. Null if none of the segments in the playlist is CDM-encrypted.\n   */\n  @Nullable public final DrmInitData protectionSchemes;\n  /**\n   * The list of segments in the playlist.\n   */\n  public final List<Segment> segments;\n  /**\n   * The total duration of the playlist in microseconds.\n   */\n  public final long durationUs;\n\n  /**\n   * @param playlistType See {@link #playlistType}.\n   * @param baseUri See {@link #baseUri}.\n   * @param tags See {@link #tags}.\n   * @param startOffsetUs See {@link #startOffsetUs}.\n   * @param startTimeUs See {@link #startTimeUs}.\n   * @param hasDiscontinuitySequence See {@link #hasDiscontinuitySequence}.\n   * @param discontinuitySequence See {@link #discontinuitySequence}.\n   * @param mediaSequence See {@link #mediaSequence}.\n   * @param version See {@link #version}.\n   * @param targetDurationUs See {@link #targetDurationUs}.\n   * @param hasIndependentSegments See {@link #hasIndependentSegments}.\n   * @param hasEndTag See {@link #hasEndTag}.\n   * @param protectionSchemes See {@link #protectionSchemes}.\n   * @param hasProgramDateTime See {@link #hasProgramDateTime}.\n   * @param segments See {@link #segments}.\n   */\n  public HlsMediaPlaylist(\n      @PlaylistType int playlistType,\n      String baseUri,\n      List<String> tags,\n      long startOffsetUs,\n      long startTimeUs,\n      boolean hasDiscontinuitySequence,\n      int discontinuitySequence,\n      long mediaSequence,\n      int version,\n      long targetDurationUs,\n      boolean hasIndependentSegments,\n      boolean hasEndTag,\n      boolean hasProgramDateTime,\n      @Nullable DrmInitData protectionSchemes,\n      List<Segment> segments) {\n    super(baseUri, tags, hasIndependentSegments);\n    this.playlistType = playlistType;\n    this.startTimeUs = startTimeUs;\n    this.hasDiscontinuitySequence = hasDiscontinuitySequence;\n    this.discontinuitySequence = discontinuitySequence;\n    this.mediaSequence = mediaSequence;\n    this.version = version;\n    this.targetDurationUs = targetDurationUs;\n    this.hasEndTag = hasEndTag;\n    this.hasProgramDateTime = hasProgramDateTime;\n    this.protectionSchemes = protectionSchemes;\n    this.segments = Collections.unmodifiableList(segments);\n    if (!segments.isEmpty()) {\n      Segment last = segments.get(segments.size() - 1);\n      durationUs = last.relativeStartTimeUs + last.durationUs;\n    } else {\n      durationUs = 0;\n    }\n    this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET\n        : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;\n  }\n\n  @Override\n  public HlsMediaPlaylist copy(List<StreamKey> streamKeys) {\n    return this;\n  }\n\n  /**\n   * Returns whether this playlist is newer than {@code other}.\n   *\n   * @param other The playlist to compare.\n   * @return Whether this playlist is newer than {@code other}.\n   */\n  public boolean isNewerThan(HlsMediaPlaylist other) {\n    if (other == null || mediaSequence > other.mediaSequence) {\n      return true;\n    }\n    if (mediaSequence < other.mediaSequence) {\n      return false;\n    }\n    // The media sequences are equal.\n    int segmentCount = segments.size();\n    int otherSegmentCount = other.segments.size();\n    return segmentCount > otherSegmentCount\n        || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);\n  }\n\n  /**\n   * Returns the result of adding the duration of the playlist to its start time.\n   */\n  public long getEndTimeUs() {\n    return startTimeUs + durationUs;\n  }\n\n  /**\n   * Returns a playlist identical to this one except for the start time, the discontinuity sequence\n   * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,\n   * {@code hasDiscontinuitySequence} is set to true.\n   *\n   * @param startTimeUs The start time for the returned playlist.\n   * @param discontinuitySequence The discontinuity sequence for the returned playlist.\n   * @return An identical playlist including the provided discontinuity and timing information.\n   */\n  public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {\n    return new HlsMediaPlaylist(\n        playlistType,\n        baseUri,\n        tags,\n        startOffsetUs,\n        startTimeUs,\n        /* hasDiscontinuitySequence= */ true,\n        discontinuitySequence,\n        mediaSequence,\n        version,\n        targetDurationUs,\n        hasIndependentSegments,\n        hasEndTag,\n        hasProgramDateTime,\n        protectionSchemes,\n        segments);\n  }\n\n  /**\n   * Returns a playlist identical to this one except that an end tag is added. If an end tag is\n   * already present then the playlist will return itself.\n   */\n  public HlsMediaPlaylist copyWithEndTag() {\n    if (this.hasEndTag) {\n      return this;\n    }\n    return new HlsMediaPlaylist(\n        playlistType,\n        baseUri,\n        tags,\n        startOffsetUs,\n        startTimeUs,\n        hasDiscontinuitySequence,\n        discontinuitySequence,\n        mediaSequence,\n        version,\n        targetDurationUs,\n        hasIndependentSegments,\n        /* hasEndTag= */ true,\n        hasProgramDateTime,\n        protectionSchemes,\n        segments);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport com.google.android.exoplayer2.offline.FilterableManifest;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Represents an HLS playlist. */\npublic abstract class HlsPlaylist implements FilterableManifest<HlsPlaylist> {\n\n  /**\n   * The base uri. Used to resolve relative paths.\n   */\n  public final String baseUri;\n  /**\n   * The list of tags in the playlist.\n   */\n  public final List<String> tags;\n  /**\n   * Whether the media is formed of independent segments, as defined by the\n   * #EXT-X-INDEPENDENT-SEGMENTS tag.\n   */\n  public final boolean hasIndependentSegments;\n\n  /**\n   * @param baseUri See {@link #baseUri}.\n   * @param tags See {@link #tags}.\n   * @param hasIndependentSegments See {@link #hasIndependentSegments}.\n   */\n  protected HlsPlaylist(String baseUri, List<String> tags, boolean hasIndependentSegments) {\n    this.baseUri = baseUri;\n    this.tags = Collections.unmodifiableList(tags);\n    this.hasIndependentSegments = hasIndependentSegments;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.UnrecognizedInputFormatException;\nimport com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;\nimport com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Queue;\nimport java.util.TreeMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;\nimport org.checkerframework.checker.nullness.qual.PolyNull;\n\n/**\n * HLS playlists parsing logic.\n */\npublic final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {\n\n  private static final String PLAYLIST_HEADER = \"#EXTM3U\";\n\n  private static final String TAG_PREFIX = \"#EXT\";\n\n  private static final String TAG_VERSION = \"#EXT-X-VERSION\";\n  private static final String TAG_PLAYLIST_TYPE = \"#EXT-X-PLAYLIST-TYPE\";\n  private static final String TAG_DEFINE = \"#EXT-X-DEFINE\";\n  private static final String TAG_STREAM_INF = \"#EXT-X-STREAM-INF\";\n  private static final String TAG_MEDIA = \"#EXT-X-MEDIA\";\n  private static final String TAG_TARGET_DURATION = \"#EXT-X-TARGETDURATION\";\n  private static final String TAG_DISCONTINUITY = \"#EXT-X-DISCONTINUITY\";\n  private static final String TAG_DISCONTINUITY_SEQUENCE = \"#EXT-X-DISCONTINUITY-SEQUENCE\";\n  private static final String TAG_PROGRAM_DATE_TIME = \"#EXT-X-PROGRAM-DATE-TIME\";\n  private static final String TAG_INIT_SEGMENT = \"#EXT-X-MAP\";\n  private static final String TAG_INDEPENDENT_SEGMENTS = \"#EXT-X-INDEPENDENT-SEGMENTS\";\n  private static final String TAG_MEDIA_DURATION = \"#EXTINF\";\n  private static final String TAG_MEDIA_SEQUENCE = \"#EXT-X-MEDIA-SEQUENCE\";\n  private static final String TAG_START = \"#EXT-X-START\";\n  private static final String TAG_ENDLIST = \"#EXT-X-ENDLIST\";\n  private static final String TAG_KEY = \"#EXT-X-KEY\";\n  private static final String TAG_SESSION_KEY = \"#EXT-X-SESSION-KEY\";\n  private static final String TAG_BYTERANGE = \"#EXT-X-BYTERANGE\";\n  private static final String TAG_GAP = \"#EXT-X-GAP\";\n\n  private static final String TYPE_AUDIO = \"AUDIO\";\n  private static final String TYPE_VIDEO = \"VIDEO\";\n  private static final String TYPE_SUBTITLES = \"SUBTITLES\";\n  private static final String TYPE_CLOSED_CAPTIONS = \"CLOSED-CAPTIONS\";\n\n  private static final String METHOD_NONE = \"NONE\";\n  private static final String METHOD_AES_128 = \"AES-128\";\n  private static final String METHOD_SAMPLE_AES = \"SAMPLE-AES\";\n  // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.\n  private static final String METHOD_SAMPLE_AES_CENC = \"SAMPLE-AES-CENC\";\n  private static final String METHOD_SAMPLE_AES_CTR = \"SAMPLE-AES-CTR\";\n  private static final String KEYFORMAT_PLAYREADY = \"com.microsoft.playready\";\n  private static final String KEYFORMAT_IDENTITY = \"identity\";\n  private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =\n      \"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\";\n  private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = \"com.widevine\";\n\n  private static final String BOOLEAN_TRUE = \"YES\";\n  private static final String BOOLEAN_FALSE = \"NO\";\n\n  private static final String ATTR_CLOSED_CAPTIONS_NONE = \"CLOSED-CAPTIONS=NONE\";\n\n  private static final Pattern REGEX_AVERAGE_BANDWIDTH =\n      Pattern.compile(\"AVERAGE-BANDWIDTH=(\\\\d+)\\\\b\");\n  private static final Pattern REGEX_VIDEO = Pattern.compile(\"VIDEO=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_AUDIO = Pattern.compile(\"AUDIO=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_SUBTITLES = Pattern.compile(\"SUBTITLES=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile(\"CLOSED-CAPTIONS=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_BANDWIDTH = Pattern.compile(\"[^-]BANDWIDTH=(\\\\d+)\\\\b\");\n  private static final Pattern REGEX_CHANNELS = Pattern.compile(\"CHANNELS=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_CODECS = Pattern.compile(\"CODECS=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_RESOLUTION = Pattern.compile(\"RESOLUTION=(\\\\d+x\\\\d+)\");\n  private static final Pattern REGEX_FRAME_RATE = Pattern.compile(\"FRAME-RATE=([\\\\d\\\\.]+)\\\\b\");\n  private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION\n      + \":(\\\\d+)\\\\b\");\n  private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + \":(\\\\d+)\\\\b\");\n  private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE\n      + \":(.+)\\\\b\");\n  private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE\n      + \":(\\\\d+)\\\\b\");\n  private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION\n      + \":([\\\\d\\\\.]+)\\\\b\");\n  private static final Pattern REGEX_MEDIA_TITLE =\n      Pattern.compile(TAG_MEDIA_DURATION + \":[\\\\d\\\\.]+\\\\b,(.+)\");\n  private static final Pattern REGEX_TIME_OFFSET = Pattern.compile(\"TIME-OFFSET=(-?[\\\\d\\\\.]+)\\\\b\");\n  private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE\n      + \":(\\\\d+(?:@\\\\d+)?)\\\\b\");\n  private static final Pattern REGEX_ATTR_BYTERANGE =\n      Pattern.compile(\"BYTERANGE=\\\"(\\\\d+(?:@\\\\d+)?)\\\\b\\\"\");\n  private static final Pattern REGEX_METHOD =\n      Pattern.compile(\n          \"METHOD=(\"\n              + METHOD_NONE\n              + \"|\"\n              + METHOD_AES_128\n              + \"|\"\n              + METHOD_SAMPLE_AES\n              + \"|\"\n              + METHOD_SAMPLE_AES_CENC\n              + \"|\"\n              + METHOD_SAMPLE_AES_CTR\n              + \")\"\n              + \"\\\\s*(?:,|$)\");\n  private static final Pattern REGEX_KEYFORMAT = Pattern.compile(\"KEYFORMAT=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_KEYFORMATVERSIONS =\n      Pattern.compile(\"KEYFORMATVERSIONS=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_URI = Pattern.compile(\"URI=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_IV = Pattern.compile(\"IV=([^,.*]+)\");\n  private static final Pattern REGEX_TYPE = Pattern.compile(\"TYPE=(\" + TYPE_AUDIO + \"|\" + TYPE_VIDEO\n      + \"|\" + TYPE_SUBTITLES + \"|\" + TYPE_CLOSED_CAPTIONS + \")\");\n  private static final Pattern REGEX_LANGUAGE = Pattern.compile(\"LANGUAGE=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_NAME = Pattern.compile(\"NAME=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_GROUP_ID = Pattern.compile(\"GROUP-ID=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile(\"CHARACTERISTICS=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_INSTREAM_ID =\n      Pattern.compile(\"INSTREAM-ID=\\\"((?:CC|SERVICE)\\\\d+)\\\"\");\n  private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern(\"AUTOSELECT\");\n  private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern(\"DEFAULT\");\n  private static final Pattern REGEX_FORCED = compileBooleanAttrPattern(\"FORCED\");\n  private static final Pattern REGEX_VALUE = Pattern.compile(\"VALUE=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_IMPORT = Pattern.compile(\"IMPORT=\\\"(.+?)\\\"\");\n  private static final Pattern REGEX_VARIABLE_REFERENCE =\n      Pattern.compile(\"\\\\{\\\\$([a-zA-Z0-9\\\\-_]+)\\\\}\");\n\n  private final HlsMasterPlaylist masterPlaylist;\n\n  /**\n   * Creates an instance where media playlists are parsed without inheriting attributes from a\n   * master playlist.\n   */\n  public HlsPlaylistParser() {\n    this(HlsMasterPlaylist.EMPTY);\n  }\n\n  /**\n   * Creates an instance where parsed media playlists inherit attributes from the given master\n   * playlist.\n   *\n   * @param masterPlaylist The master playlist from which media playlists will inherit attributes.\n   */\n  public HlsPlaylistParser(HlsMasterPlaylist masterPlaylist) {\n    this.masterPlaylist = masterPlaylist;\n  }\n\n  @Override\n  public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {\n    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));\n    Queue<String> extraLines = new ArrayDeque<>();\n    String line;\n    try {\n      if (!checkPlaylistHeader(reader)) {\n        throw new UnrecognizedInputFormatException(\"Input does not start with the #EXTM3U header.\",\n            uri);\n      }\n      while ((line = reader.readLine()) != null) {\n        line = line.trim();\n        if (line.isEmpty()) {\n          // Do nothing.\n        } else if (line.startsWith(TAG_STREAM_INF)) {\n          extraLines.add(line);\n          return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString());\n        } else if (line.startsWith(TAG_TARGET_DURATION)\n            || line.startsWith(TAG_MEDIA_SEQUENCE)\n            || line.startsWith(TAG_MEDIA_DURATION)\n            || line.startsWith(TAG_KEY)\n            || line.startsWith(TAG_BYTERANGE)\n            || line.equals(TAG_DISCONTINUITY)\n            || line.equals(TAG_DISCONTINUITY_SEQUENCE)\n            || line.equals(TAG_ENDLIST)) {\n          extraLines.add(line);\n          return parseMediaPlaylist(\n              masterPlaylist, new LineIterator(extraLines, reader), uri.toString());\n        } else {\n          extraLines.add(line);\n        }\n      }\n    } finally {\n      Util.closeQuietly(reader);\n    }\n    throw new ParserException(\"Failed to parse the playlist, could not identify any tags.\");\n  }\n\n  private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {\n    int last = reader.read();\n    if (last == 0xEF) {\n      if (reader.read() != 0xBB || reader.read() != 0xBF) {\n        return false;\n      }\n      // The playlist contains a Byte Order Mark, which gets discarded.\n      last = reader.read();\n    }\n    last = skipIgnorableWhitespace(reader, true, last);\n    int playlistHeaderLength = PLAYLIST_HEADER.length();\n    for (int i = 0; i < playlistHeaderLength; i++) {\n      if (last != PLAYLIST_HEADER.charAt(i)) {\n        return false;\n      }\n      last = reader.read();\n    }\n    last = skipIgnorableWhitespace(reader, false, last);\n    return Util.isLinebreak(last);\n  }\n\n  private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)\n      throws IOException {\n    while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {\n      c = reader.read();\n    }\n    return c;\n  }\n\n  private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)\n      throws IOException {\n    HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();\n    HashMap<String, String> variableDefinitions = new HashMap<>();\n    ArrayList<Variant> variants = new ArrayList<>();\n    ArrayList<Rendition> videos = new ArrayList<>();\n    ArrayList<Rendition> audios = new ArrayList<>();\n    ArrayList<Rendition> subtitles = new ArrayList<>();\n    ArrayList<Rendition> closedCaptions = new ArrayList<>();\n    ArrayList<String> mediaTags = new ArrayList<>();\n    ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();\n    ArrayList<String> tags = new ArrayList<>();\n    Format muxedAudioFormat = null;\n    List<Format> muxedCaptionFormats = null;\n    boolean noClosedCaptions = false;\n    boolean hasIndependentSegmentsTag = false;\n\n    String line;\n    while (iterator.hasNext()) {\n      line = iterator.next();\n\n      if (line.startsWith(TAG_PREFIX)) {\n        // We expose all tags through the playlist.\n        tags.add(line);\n      }\n\n      if (line.startsWith(TAG_DEFINE)) {\n        variableDefinitions.put(\n            /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions),\n            /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions));\n      } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {\n        hasIndependentSegmentsTag = true;\n      } else if (line.startsWith(TAG_MEDIA)) {\n        // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF\n        // tags.\n        mediaTags.add(line);\n      } else if (line.startsWith(TAG_SESSION_KEY)) {\n        String keyFormat =\n            parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);\n        SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);\n        if (schemeData != null) {\n          String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);\n          String scheme = parseEncryptionScheme(method);\n          sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));\n        }\n      } else if (line.startsWith(TAG_STREAM_INF)) {\n        noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);\n        int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);\n        // TODO: Plumb this into Format.\n        int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);\n        String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);\n        String resolutionString =\n            parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions);\n        int width;\n        int height;\n        if (resolutionString != null) {\n          String[] widthAndHeight = resolutionString.split(\"x\");\n          width = Integer.parseInt(widthAndHeight[0]);\n          height = Integer.parseInt(widthAndHeight[1]);\n          if (width <= 0 || height <= 0) {\n            // Resolution string is invalid.\n            width = Format.NO_VALUE;\n            height = Format.NO_VALUE;\n          }\n        } else {\n          width = Format.NO_VALUE;\n          height = Format.NO_VALUE;\n        }\n        float frameRate = Format.NO_VALUE;\n        String frameRateString =\n            parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);\n        if (frameRateString != null) {\n          frameRate = Float.parseFloat(frameRateString);\n        }\n        String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);\n        String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);\n        String subtitlesGroupId =\n            parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);\n        String closedCaptionsGroupId =\n            parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);\n        if (!iterator.hasNext()) {\n          throw new ParserException(\"#EXT-X-STREAM-INF tag must be followed by another line\");\n        }\n        line =\n            replaceVariableReferences(\n                iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.\n        Uri uri = UriUtil.resolveToUri(baseUri, line);\n        Format format =\n            Format.createVideoContainerFormat(\n                /* id= */ Integer.toString(variants.size()),\n                /* label= */ null,\n                /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n                /* sampleMimeType= */ null,\n                codecs,\n                /* metadata= */ null,\n                bitrate,\n                width,\n                height,\n                frameRate,\n                /* initializationData= */ null,\n                /* selectionFlags= */ 0,\n                /* roleFlags= */ 0);\n        Variant variant =\n            new Variant(\n                uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId);\n        variants.add(variant);\n        ArrayList<VariantInfo> variantInfosForUrl = urlToVariantInfos.get(uri);\n        if (variantInfosForUrl == null) {\n          variantInfosForUrl = new ArrayList<>();\n          urlToVariantInfos.put(uri, variantInfosForUrl);\n        }\n        variantInfosForUrl.add(\n            new VariantInfo(\n                bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId));\n      }\n    }\n\n    // TODO: Don't deduplicate variants by URL.\n    ArrayList<Variant> deduplicatedVariants = new ArrayList<>();\n    HashSet<Uri> urlsInDeduplicatedVariants = new HashSet<>();\n    for (int i = 0; i < variants.size(); i++) {\n      Variant variant = variants.get(i);\n      if (urlsInDeduplicatedVariants.add(variant.url)) {\n        Assertions.checkState(variant.format.metadata == null);\n        HlsTrackMetadataEntry hlsMetadataEntry =\n            new HlsTrackMetadataEntry(\n                /* groupId= */ null,\n                /* name= */ null,\n                Assertions.checkNotNull(urlToVariantInfos.get(variant.url)));\n        deduplicatedVariants.add(\n            variant.copyWithFormat(\n                variant.format.copyWithMetadata(new Metadata(hlsMetadataEntry))));\n      }\n    }\n\n    for (int i = 0; i < mediaTags.size(); i++) {\n      line = mediaTags.get(i);\n      String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions);\n      String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);\n      String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);\n      Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri);\n      String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions);\n      @C.SelectionFlags int selectionFlags = parseSelectionFlags(line);\n      @C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions);\n      String formatId = groupId + \":\" + name;\n      Format format;\n      Metadata metadata =\n          new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList()));\n      switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {\n        case TYPE_VIDEO:\n          Variant variant = getVariantWithVideoGroup(variants, groupId);\n          String codecs = null;\n          int width = Format.NO_VALUE;\n          int height = Format.NO_VALUE;\n          float frameRate = Format.NO_VALUE;\n          if (variant != null) {\n            Format variantFormat = variant.format;\n            codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);\n            width = variantFormat.width;\n            height = variantFormat.height;\n            frameRate = variantFormat.frameRate;\n          }\n          String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;\n          format =\n              Format.createVideoContainerFormat(\n                      /* id= */ formatId,\n                      /* label= */ name,\n                      /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n                      sampleMimeType,\n                      codecs,\n                      /* metadata= */ null,\n                      /* bitrate= */ Format.NO_VALUE,\n                      width,\n                      height,\n                      frameRate,\n                      /* initializationData= */ null,\n                      selectionFlags,\n                      roleFlags)\n                  .copyWithMetadata(metadata);\n          if (uri == null) {\n            // TODO: Remove this case and add a Rendition with a null uri to videos.\n          } else {\n            videos.add(new Rendition(uri, format, groupId, name));\n          }\n          break;\n        case TYPE_AUDIO:\n          variant = getVariantWithAudioGroup(variants, groupId);\n          codecs =\n              variant != null\n                  ? Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO)\n                  : null;\n          sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;\n          String channelsString =\n              parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions);\n          int channelCount = Format.NO_VALUE;\n          if (channelsString != null) {\n            channelCount = Integer.parseInt(Util.splitAtFirst(channelsString, \"/\")[0]);\n            if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType) && channelsString.endsWith(\"/JOC\")) {\n              sampleMimeType = MimeTypes.AUDIO_E_AC3_JOC;\n            }\n          }\n          format =\n              Format.createAudioContainerFormat(\n                  /* id= */ formatId,\n                  /* label= */ name,\n                  /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n                  sampleMimeType,\n                  codecs,\n                  /* metadata= */ null,\n                  /* bitrate= */ Format.NO_VALUE,\n                  channelCount,\n                  /* sampleRate= */ Format.NO_VALUE,\n                  /* initializationData= */ null,\n                  selectionFlags,\n                  roleFlags,\n                  language);\n          if (uri == null) {\n            // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.\n            muxedAudioFormat = format;\n          } else {\n            audios.add(new Rendition(uri, format.copyWithMetadata(metadata), groupId, name));\n          }\n          break;\n        case TYPE_SUBTITLES:\n          format =\n              Format.createTextContainerFormat(\n                      /* id= */ formatId,\n                      /* label= */ name,\n                      /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n                      /* sampleMimeType= */ MimeTypes.TEXT_VTT,\n                      /* codecs= */ null,\n                      /* bitrate= */ Format.NO_VALUE,\n                      selectionFlags,\n                      roleFlags,\n                      language)\n                  .copyWithMetadata(metadata);\n          subtitles.add(new Rendition(uri, format, groupId, name));\n          break;\n        case TYPE_CLOSED_CAPTIONS:\n          String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);\n          String mimeType;\n          int accessibilityChannel;\n          if (instreamId.startsWith(\"CC\")) {\n            mimeType = MimeTypes.APPLICATION_CEA608;\n            accessibilityChannel = Integer.parseInt(instreamId.substring(2));\n          } else /* starts with SERVICE */ {\n            mimeType = MimeTypes.APPLICATION_CEA708;\n            accessibilityChannel = Integer.parseInt(instreamId.substring(7));\n          }\n          if (muxedCaptionFormats == null) {\n            muxedCaptionFormats = new ArrayList<>();\n          }\n          muxedCaptionFormats.add(\n              Format.createTextContainerFormat(\n                  /* id= */ formatId,\n                  /* label= */ name,\n                  /* containerMimeType= */ null,\n                  /* sampleMimeType= */ mimeType,\n                  /* codecs= */ null,\n                  /* bitrate= */ Format.NO_VALUE,\n                  selectionFlags,\n                  roleFlags,\n                  language,\n                  accessibilityChannel));\n          // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions.\n          break;\n        default:\n          // Do nothing.\n          break;\n      }\n    }\n\n    if (noClosedCaptions) {\n      muxedCaptionFormats = Collections.emptyList();\n    }\n\n    return new HlsMasterPlaylist(\n        baseUri,\n        tags,\n        deduplicatedVariants,\n        videos,\n        audios,\n        subtitles,\n        closedCaptions,\n        muxedAudioFormat,\n        muxedCaptionFormats,\n        hasIndependentSegmentsTag,\n        variableDefinitions,\n        sessionKeyDrmInitData);\n  }\n\n  @Nullable\n  private static Variant getVariantWithAudioGroup(ArrayList<Variant> variants, String groupId) {\n    for (int i = 0; i < variants.size(); i++) {\n      Variant variant = variants.get(i);\n      if (groupId.equals(variant.audioGroupId)) {\n        return variant;\n      }\n    }\n    return null;\n  }\n\n  @Nullable\n  private static Variant getVariantWithVideoGroup(ArrayList<Variant> variants, String groupId) {\n    for (int i = 0; i < variants.size(); i++) {\n      Variant variant = variants.get(i);\n      if (groupId.equals(variant.videoGroupId)) {\n        return variant;\n      }\n    }\n    return null;\n  }\n\n  private static HlsMediaPlaylist parseMediaPlaylist(\n      HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {\n    @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;\n    long startOffsetUs = C.TIME_UNSET;\n    long mediaSequence = 0;\n    int version = 1; // Default version == 1.\n    long targetDurationUs = C.TIME_UNSET;\n    boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;\n    boolean hasEndTag = false;\n    Segment initializationSegment = null;\n    HashMap<String, String> variableDefinitions = new HashMap<>();\n    List<Segment> segments = new ArrayList<>();\n    List<String> tags = new ArrayList<>();\n\n    long segmentDurationUs = 0;\n    String segmentTitle = \"\";\n    boolean hasDiscontinuitySequence = false;\n    int playlistDiscontinuitySequence = 0;\n    int relativeDiscontinuitySequence = 0;\n    long playlistStartTimeUs = 0;\n    long segmentStartTimeUs = 0;\n    long segmentByteRangeOffset = 0;\n    long segmentByteRangeLength = C.LENGTH_UNSET;\n    long segmentMediaSequence = 0;\n    boolean hasGapTag = false;\n\n    DrmInitData playlistProtectionSchemes = null;\n    String fullSegmentEncryptionKeyUri = null;\n    String fullSegmentEncryptionIV = null;\n    TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();\n    String encryptionScheme = null;\n    DrmInitData cachedDrmInitData = null;\n\n    String line;\n    while (iterator.hasNext()) {\n      line = iterator.next();\n\n      if (line.startsWith(TAG_PREFIX)) {\n        // We expose all tags through the playlist.\n        tags.add(line);\n      }\n\n      if (line.startsWith(TAG_PLAYLIST_TYPE)) {\n        String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);\n        if (\"VOD\".equals(playlistTypeString)) {\n          playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;\n        } else if (\"EVENT\".equals(playlistTypeString)) {\n          playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;\n        }\n      } else if (line.startsWith(TAG_START)) {\n        startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);\n      } else if (line.startsWith(TAG_INIT_SEGMENT)) {\n        String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);\n        String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);\n        if (byteRange != null) {\n          String[] splitByteRange = byteRange.split(\"@\");\n          segmentByteRangeLength = Long.parseLong(splitByteRange[0]);\n          if (splitByteRange.length > 1) {\n            segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);\n          }\n        }\n        if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {\n          // See RFC 8216, Section 4.3.2.5.\n          throw new ParserException(\n              \"The encryption IV attribute must be present when an initialization segment is \"\n                  + \"encrypted with METHOD=AES-128.\");\n        }\n        initializationSegment =\n            new Segment(\n                uri,\n                segmentByteRangeOffset,\n                segmentByteRangeLength,\n                fullSegmentEncryptionKeyUri,\n                fullSegmentEncryptionIV);\n        segmentByteRangeOffset = 0;\n        segmentByteRangeLength = C.LENGTH_UNSET;\n      } else if (line.startsWith(TAG_TARGET_DURATION)) {\n        targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;\n      } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {\n        mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);\n        segmentMediaSequence = mediaSequence;\n      } else if (line.startsWith(TAG_VERSION)) {\n        version = parseIntAttr(line, REGEX_VERSION);\n      } else if (line.startsWith(TAG_DEFINE)) {\n        String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);\n        if (importName != null) {\n          String value = masterPlaylist.variableDefinitions.get(importName);\n          if (value != null) {\n            variableDefinitions.put(importName, value);\n          } else {\n            // The master playlist does not declare the imported variable. Ignore.\n          }\n        } else {\n          variableDefinitions.put(\n              parseStringAttr(line, REGEX_NAME, variableDefinitions),\n              parseStringAttr(line, REGEX_VALUE, variableDefinitions));\n        }\n      } else if (line.startsWith(TAG_MEDIA_DURATION)) {\n        segmentDurationUs =\n            (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);\n        segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, \"\", variableDefinitions);\n      } else if (line.startsWith(TAG_KEY)) {\n        String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);\n        String keyFormat =\n            parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);\n        fullSegmentEncryptionKeyUri = null;\n        fullSegmentEncryptionIV = null;\n        if (METHOD_NONE.equals(method)) {\n          currentSchemeDatas.clear();\n          cachedDrmInitData = null;\n        } else /* !METHOD_NONE.equals(method) */ {\n          fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);\n          if (KEYFORMAT_IDENTITY.equals(keyFormat)) {\n            if (METHOD_AES_128.equals(method)) {\n              // The segment is fully encrypted using an identity key.\n              fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);\n            } else {\n              // Do nothing. Samples are encrypted using an identity key, but this is not supported.\n              // Hopefully, a traditional DRM alternative is also provided.\n            }\n          } else {\n            if (encryptionScheme == null) {\n              encryptionScheme = parseEncryptionScheme(method);\n            }\n            SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);\n            if (schemeData != null) {\n              cachedDrmInitData = null;\n              currentSchemeDatas.put(keyFormat, schemeData);\n            }\n          }\n        }\n      } else if (line.startsWith(TAG_BYTERANGE)) {\n        String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);\n        String[] splitByteRange = byteRange.split(\"@\");\n        segmentByteRangeLength = Long.parseLong(splitByteRange[0]);\n        if (splitByteRange.length > 1) {\n          segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);\n        }\n      } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {\n        hasDiscontinuitySequence = true;\n        playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));\n      } else if (line.equals(TAG_DISCONTINUITY)) {\n        relativeDiscontinuitySequence++;\n      } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {\n        if (playlistStartTimeUs == 0) {\n          long programDatetimeUs =\n              C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));\n          playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;\n        }\n      } else if (line.equals(TAG_GAP)) {\n        hasGapTag = true;\n      } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {\n        hasIndependentSegmentsTag = true;\n      } else if (line.equals(TAG_ENDLIST)) {\n        hasEndTag = true;\n      } else if (!line.startsWith(\"#\")) {\n        String segmentEncryptionIV;\n        if (fullSegmentEncryptionKeyUri == null) {\n          segmentEncryptionIV = null;\n        } else if (fullSegmentEncryptionIV != null) {\n          segmentEncryptionIV = fullSegmentEncryptionIV;\n        } else {\n          segmentEncryptionIV = Long.toHexString(segmentMediaSequence);\n        }\n\n        segmentMediaSequence++;\n        if (segmentByteRangeLength == C.LENGTH_UNSET) {\n          segmentByteRangeOffset = 0;\n        }\n\n        if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {\n          SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);\n          cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);\n          if (playlistProtectionSchemes == null) {\n            SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];\n            for (int i = 0; i < schemeDatas.length; i++) {\n              playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);\n            }\n            playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);\n          }\n        }\n\n        segments.add(\n            new Segment(\n                replaceVariableReferences(line, variableDefinitions),\n                initializationSegment,\n                segmentTitle,\n                segmentDurationUs,\n                relativeDiscontinuitySequence,\n                segmentStartTimeUs,\n                cachedDrmInitData,\n                fullSegmentEncryptionKeyUri,\n                segmentEncryptionIV,\n                segmentByteRangeOffset,\n                segmentByteRangeLength,\n                hasGapTag));\n        segmentStartTimeUs += segmentDurationUs;\n        segmentDurationUs = 0;\n        segmentTitle = \"\";\n        if (segmentByteRangeLength != C.LENGTH_UNSET) {\n          segmentByteRangeOffset += segmentByteRangeLength;\n        }\n        segmentByteRangeLength = C.LENGTH_UNSET;\n        hasGapTag = false;\n      }\n    }\n    return new HlsMediaPlaylist(\n        playlistType,\n        baseUri,\n        tags,\n        startOffsetUs,\n        playlistStartTimeUs,\n        hasDiscontinuitySequence,\n        playlistDiscontinuitySequence,\n        mediaSequence,\n        version,\n        targetDurationUs,\n        hasIndependentSegmentsTag,\n        hasEndTag,\n        /* hasProgramDateTime= */ playlistStartTimeUs != 0,\n        playlistProtectionSchemes,\n        segments);\n  }\n\n  @C.SelectionFlags\n  private static int parseSelectionFlags(String line) {\n    int flags = 0;\n    if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {\n      flags |= C.SELECTION_FLAG_DEFAULT;\n    }\n    if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {\n      flags |= C.SELECTION_FLAG_FORCED;\n    }\n    if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {\n      flags |= C.SELECTION_FLAG_AUTOSELECT;\n    }\n    return flags;\n  }\n\n  @C.RoleFlags\n  private static int parseRoleFlags(String line, Map<String, String> variableDefinitions) {\n    String concatenatedCharacteristics =\n        parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions);\n    if (TextUtils.isEmpty(concatenatedCharacteristics)) {\n      return 0;\n    }\n    String[] characteristics = Util.split(concatenatedCharacteristics, \",\");\n    @C.RoleFlags int roleFlags = 0;\n    if (Util.contains(characteristics, \"public.accessibility.describes-video\")) {\n      roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO;\n    }\n    if (Util.contains(characteristics, \"public.accessibility.transcribes-spoken-dialog\")) {\n      roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG;\n    }\n    if (Util.contains(characteristics, \"public.accessibility.describes-music-and-sound\")) {\n      roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;\n    }\n    if (Util.contains(characteristics, \"public.easy-to-read\")) {\n      roleFlags |= C.ROLE_FLAG_EASY_TO_READ;\n    }\n    return roleFlags;\n  }\n\n  @Nullable\n  private static SchemeData parseDrmSchemeData(\n      String line, String keyFormat, Map<String, String> variableDefinitions)\n      throws ParserException {\n    String keyFormatVersions =\n        parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, \"1\", variableDefinitions);\n    if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {\n      String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);\n      return new SchemeData(\n          C.WIDEVINE_UUID,\n          MimeTypes.VIDEO_MP4,\n          Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));\n    } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {\n      return new SchemeData(C.WIDEVINE_UUID, \"hls\", Util.getUtf8Bytes(line));\n    } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && \"1\".equals(keyFormatVersions)) {\n      String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);\n      byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);\n      byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);\n      return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);\n    }\n    return null;\n  }\n\n  private static String parseEncryptionScheme(String method) {\n    return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)\n        ? C.CENC_TYPE_cenc\n        : C.CENC_TYPE_cbcs;\n  }\n\n  private static int parseIntAttr(String line, Pattern pattern) throws ParserException {\n    return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));\n  }\n\n  private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) {\n    Matcher matcher = pattern.matcher(line);\n    if (matcher.find()) {\n      return Integer.parseInt(matcher.group(1));\n    }\n    return defaultValue;\n  }\n\n  private static long parseLongAttr(String line, Pattern pattern) throws ParserException {\n    return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));\n  }\n\n  private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {\n    return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));\n  }\n\n  private static String parseStringAttr(\n      String line, Pattern pattern, Map<String, String> variableDefinitions)\n      throws ParserException {\n    String value = parseOptionalStringAttr(line, pattern, variableDefinitions);\n    if (value != null) {\n      return value;\n    } else {\n      throw new ParserException(\"Couldn't match \" + pattern.pattern() + \" in \" + line);\n    }\n  }\n\n  private static @Nullable String parseOptionalStringAttr(\n      String line, Pattern pattern, Map<String, String> variableDefinitions) {\n    return parseOptionalStringAttr(line, pattern, null, variableDefinitions);\n  }\n\n  private static @PolyNull String parseOptionalStringAttr(\n      String line,\n      Pattern pattern,\n      @PolyNull String defaultValue,\n      Map<String, String> variableDefinitions) {\n    Matcher matcher = pattern.matcher(line);\n    String value = matcher.find() ? matcher.group(1) : defaultValue;\n    return variableDefinitions.isEmpty() || value == null\n        ? value\n        : replaceVariableReferences(value, variableDefinitions);\n  }\n\n  private static String replaceVariableReferences(\n      String string, Map<String, String> variableDefinitions) {\n    Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);\n    // TODO: Replace StringBuffer with StringBuilder once Java 9 is available.\n    StringBuffer stringWithReplacements = new StringBuffer();\n    while (matcher.find()) {\n      String groupName = matcher.group(1);\n      if (variableDefinitions.containsKey(groupName)) {\n        matcher.appendReplacement(\n            stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName)));\n      } else {\n        // The variable is not defined. The value is ignored.\n      }\n    }\n    matcher.appendTail(stringWithReplacements);\n    return stringWithReplacements.toString();\n  }\n\n  private static boolean parseOptionalBooleanAttribute(\n      String line, Pattern pattern, boolean defaultValue) {\n    Matcher matcher = pattern.matcher(line);\n    if (matcher.find()) {\n      return matcher.group(1).equals(BOOLEAN_TRUE);\n    }\n    return defaultValue;\n  }\n\n  private static Pattern compileBooleanAttrPattern(String attribute) {\n    return Pattern.compile(attribute + \"=(\" + BOOLEAN_FALSE + \"|\" + BOOLEAN_TRUE + \")\");\n  }\n\n  private static class LineIterator {\n\n    private final BufferedReader reader;\n    private final Queue<String> extraLines;\n\n    @Nullable private String next;\n\n    public LineIterator(Queue<String> extraLines, BufferedReader reader) {\n      this.extraLines = extraLines;\n      this.reader = reader;\n    }\n\n    @EnsuresNonNullIf(expression = \"next\", result = true)\n    public boolean hasNext() throws IOException {\n      if (next != null) {\n        return true;\n      }\n      if (!extraLines.isEmpty()) {\n        next = Assertions.checkNotNull(extraLines.poll());\n        return true;\n      }\n      while ((next = reader.readLine()) != null) {\n        next = next.trim();\n        if (!next.isEmpty()) {\n          return true;\n        }\n      }\n      return false;\n    }\n\n    /** Return the next line, or throw {@link NoSuchElementException} if none. */\n    public String next() throws IOException {\n      if (hasNext()) {\n        String result = next;\n        next = null;\n        return result;\n      } else {\n        throw new NoSuchElementException();\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\n\n/** Factory for {@link HlsPlaylist} parsers. */\npublic interface HlsPlaylistParserFactory {\n\n  /**\n   * Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit\n   * any attributes from other playlists.\n   */\n  ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser();\n\n  /**\n   * Returns a playlist parser for playlists that were referenced by the given {@link\n   * HlsMasterPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes from\n   * {@code masterPlaylist}.\n   *\n   * @param masterPlaylist The master playlist that referenced any parsed media playlists.\n   * @return A parser for HLS playlists.\n   */\n  ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(HlsMasterPlaylist masterPlaylist);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport java.io.IOException;\n\n/**\n * Tracks playlists associated to an HLS stream and provides snapshots.\n *\n * <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the\n * segments that one of the playlists exposes. This playlist is called primary and needs to be\n * periodically refreshed in the case of live streams. Note that the primary playlist is one of the\n * media playlists while the master playlist is an optional kind of playlist defined by the HLS\n * specification (RFC 8216).\n *\n * <p>Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a\n * primary playlist is always available.\n */\npublic interface HlsPlaylistTracker {\n\n  /** Factory for {@link HlsPlaylistTracker} instances. */\n  interface Factory {\n\n    /**\n     * Creates a new tracker instance.\n     *\n     * @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading.\n     * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors.\n     * @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing.\n     */\n    HlsPlaylistTracker createTracker(\n        HlsDataSourceFactory dataSourceFactory,\n        LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n        HlsPlaylistParserFactory playlistParserFactory);\n  }\n\n  /** Listener for primary playlist changes. */\n  interface PrimaryPlaylistListener {\n\n    /**\n     * Called when the primary playlist changes.\n     *\n     * @param mediaPlaylist The primary playlist new snapshot.\n     */\n    void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);\n  }\n\n  /** Called on playlist loading events. */\n  interface PlaylistEventListener {\n\n    /**\n     * Called a playlist changes.\n     */\n    void onPlaylistChanged();\n\n    /**\n     * Called if an error is encountered while loading a playlist.\n     *\n     * @param url The loaded url that caused the error.\n     * @param blacklistDurationMs The duration for which the playlist should be blacklisted. Or\n     *     {@link C#TIME_UNSET} if the playlist should not be blacklisted.\n     * @return True if blacklisting did not encounter errors. False otherwise.\n     */\n    boolean onPlaylistError(Uri url, long blacklistDurationMs);\n  }\n\n  /** Thrown when a playlist is considered to be stuck due to a server side error. */\n  final class PlaylistStuckException extends IOException {\n\n    /** The url of the stuck playlist. */\n    public final Uri url;\n\n    /**\n     * Creates an instance.\n     *\n     * @param url See {@link #url}.\n     */\n    public PlaylistStuckException(Uri url) {\n      this.url = url;\n    }\n  }\n\n  /** Thrown when the media sequence of a new snapshot indicates the server has reset. */\n  final class PlaylistResetException extends IOException {\n\n    /** The url of the reset playlist. */\n    public final Uri url;\n\n    /**\n     * Creates an instance.\n     *\n     * @param url See {@link #url}.\n     */\n    public PlaylistResetException(Uri url) {\n      this.url = url;\n    }\n  }\n\n  /**\n   * Starts the playlist tracker.\n   *\n   * <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}\n   * call.\n   *\n   * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master\n   *     playlist.\n   * @param eventDispatcher A dispatcher to notify of events.\n   * @param listener A callback for the primary playlist change events.\n   */\n  void start(\n      Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);\n\n  /**\n   * Stops the playlist tracker and releases any acquired resources.\n   *\n   * <p>Must be called once per {@link #start} call.\n   */\n  void stop();\n\n  /**\n   * Registers a listener to receive events from the playlist tracker.\n   *\n   * @param listener The listener.\n   */\n  void addListener(PlaylistEventListener listener);\n\n  /**\n   * Unregisters a listener.\n   *\n   * @param listener The listener to unregister.\n   */\n  void removeListener(PlaylistEventListener listener);\n\n  /**\n   * Returns the master playlist.\n   *\n   * <p>If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist}\n   * with a single variant for said media playlist is returned.\n   *\n   * @return The master playlist. Null if the initial playlist has yet to be loaded.\n   */\n  @Nullable\n  HlsMasterPlaylist getMasterPlaylist();\n\n  /**\n   * Returns the most recent snapshot available of the playlist referenced by the provided {@link\n   * Uri}.\n   *\n   * @param url The {@link Uri} corresponding to the requested media playlist.\n   * @param isForPlayback Whether the caller might use the snapshot to request media segments for\n   *     playback. If true, the primary playlist may be updated to the one requested.\n   * @return The most recent snapshot of the playlist referenced by the provided {@link Uri}. May be\n   *     null if no snapshot has been loaded yet.\n   */\n  @Nullable\n  HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback);\n\n  /**\n   * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no\n   * media playlist has been loaded.\n   */\n  long getInitialStartTimeUs();\n\n  /**\n   * Returns whether the snapshot of the playlist referenced by the provided {@link Uri} is valid,\n   * meaning all the segments referenced by the playlist are expected to be available. If the\n   * playlist is not valid then some of the segments may no longer be available.\n   *\n   * @param url The {@link Uri}.\n   * @return Whether the snapshot of the playlist referenced by the provided {@link Uri} is valid.\n   */\n  boolean isSnapshotValid(Uri url);\n\n  /**\n   * If the tracker is having trouble refreshing the master playlist or the primary playlist, this\n   * method throws the underlying error. Otherwise, does nothing.\n   *\n   * @throws IOException The underlying error.\n   */\n  void maybeThrowPrimaryPlaylistRefreshError() throws IOException;\n\n  /**\n   * If the playlist is having trouble refreshing the playlist referenced by the given {@link Uri},\n   * this method throws the underlying error.\n   *\n   * @param url The {@link Uri}.\n   * @throws IOException The underyling error.\n   */\n  void maybeThrowPlaylistRefreshError(Uri url) throws IOException;\n\n  /**\n   * Requests a playlist refresh and whitelists it.\n   *\n   * <p>The playlist tracker may choose the delay the playlist refresh. The request is discarded if\n   * a refresh was already pending.\n   *\n   * @param url The {@link Uri} of the playlist to be refreshed.\n   */\n  void refreshPlaylist(Uri url);\n\n  /**\n   * Returns whether the tracked playlists describe a live stream.\n   *\n   * @return True if the content is live. False otherwise.\n   */\n  boolean isLive();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.hls.playlist;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\nimport com.google.android.exoplayer2.extractor.mp4.Track;\nimport com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;\nimport com.google.android.exoplayer2.source.BehindLiveWindowException;\nimport com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;\nimport com.google.android.exoplayer2.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;\nimport com.google.android.exoplayer2.source.chunk.ChunkHolder;\nimport com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * A default {@link SsChunkSource} implementation.\n */\npublic class DefaultSsChunkSource implements SsChunkSource {\n\n  public static final class Factory implements SsChunkSource.Factory {\n\n    private final DataSource.Factory dataSourceFactory;\n\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this.dataSourceFactory = dataSourceFactory;\n    }\n\n    @Override\n    public SsChunkSource createChunkSource(\n        LoaderErrorThrower manifestLoaderErrorThrower,\n        SsManifest manifest,\n        int elementIndex,\n        TrackSelection trackSelection,\n        @Nullable TransferListener transferListener) {\n      DataSource dataSource = dataSourceFactory.createDataSource();\n      if (transferListener != null) {\n        dataSource.addTransferListener(transferListener);\n      }\n      return new DefaultSsChunkSource(\n          manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource);\n    }\n\n  }\n\n  private final LoaderErrorThrower manifestLoaderErrorThrower;\n  private final int streamElementIndex;\n  private final ChunkExtractorWrapper[] extractorWrappers;\n  private final DataSource dataSource;\n\n  private TrackSelection trackSelection;\n  private SsManifest manifest;\n  private int currentManifestChunkOffset;\n\n  private IOException fatalError;\n\n  /**\n   * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.\n   * @param manifest The initial manifest.\n   * @param streamElementIndex The index of the stream element in the manifest.\n   * @param trackSelection The track selection.\n   * @param dataSource A {@link DataSource} suitable for loading the media data.\n   */\n  public DefaultSsChunkSource(\n      LoaderErrorThrower manifestLoaderErrorThrower,\n      SsManifest manifest,\n      int streamElementIndex,\n      TrackSelection trackSelection,\n      DataSource dataSource) {\n    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\n    this.manifest = manifest;\n    this.streamElementIndex = streamElementIndex;\n    this.trackSelection = trackSelection;\n    this.dataSource = dataSource;\n\n    StreamElement streamElement = manifest.streamElements[streamElementIndex];\n    extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];\n    for (int i = 0; i < extractorWrappers.length; i++) {\n      int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);\n      Format format = streamElement.formats[manifestTrackIndex];\n      TrackEncryptionBox[] trackEncryptionBoxes =\n          format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null;\n      int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0;\n      Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,\n          C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE,\n          trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);\n      FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(\n          FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME\n          | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track, null);\n      extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format);\n    }\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    StreamElement streamElement = manifest.streamElements[streamElementIndex];\n    int chunkIndex = streamElement.getChunkIndex(positionUs);\n    long firstSyncUs = streamElement.getStartTimeUs(chunkIndex);\n    long secondSyncUs =\n        firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1\n            ? streamElement.getStartTimeUs(chunkIndex + 1)\n            : firstSyncUs;\n    return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);\n  }\n\n  @Override\n  public void updateManifest(SsManifest newManifest) {\n    StreamElement currentElement = manifest.streamElements[streamElementIndex];\n    int currentElementChunkCount = currentElement.chunkCount;\n    StreamElement newElement = newManifest.streamElements[streamElementIndex];\n    if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {\n      // There's no overlap between the old and new elements because at least one is empty.\n      currentManifestChunkOffset += currentElementChunkCount;\n    } else {\n      long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1)\n          + currentElement.getChunkDurationUs(currentElementChunkCount - 1);\n      long newElementStartTimeUs = newElement.getStartTimeUs(0);\n      if (currentElementEndTimeUs <= newElementStartTimeUs) {\n        // There's no overlap between the old and new elements.\n        currentManifestChunkOffset += currentElementChunkCount;\n      } else {\n        // The new element overlaps with the old one.\n        currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);\n      }\n    }\n    manifest = newManifest;\n  }\n\n  @Override\n  public void updateTrackSelection(TrackSelection trackSelection) {\n    this.trackSelection = trackSelection;\n  }\n\n  // ChunkSource implementation.\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    if (fatalError != null) {\n      throw fatalError;\n    } else {\n      manifestLoaderErrorThrower.maybeThrowError();\n    }\n  }\n\n  @Override\n  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\n    if (fatalError != null || trackSelection.length() < 2) {\n      return queue.size();\n    }\n    return trackSelection.evaluateQueueSize(playbackPositionUs, queue);\n  }\n\n  @Override\n  public final void getNextChunk(\n      long playbackPositionUs,\n      long loadPositionUs,\n      List<? extends MediaChunk> queue,\n      ChunkHolder out) {\n    if (fatalError != null) {\n      return;\n    }\n\n    StreamElement streamElement = manifest.streamElements[streamElementIndex];\n    if (streamElement.chunkCount == 0) {\n      // There aren't any chunks for us to load.\n      out.endOfStream = !manifest.isLive;\n      return;\n    }\n\n    int chunkIndex;\n    if (queue.isEmpty()) {\n      chunkIndex = streamElement.getChunkIndex(loadPositionUs);\n    } else {\n      chunkIndex =\n          (int) (queue.get(queue.size() - 1).getNextChunkIndex() - currentManifestChunkOffset);\n      if (chunkIndex < 0) {\n        // This is before the first chunk in the current manifest.\n        fatalError = new BehindLiveWindowException();\n        return;\n      }\n    }\n\n    if (chunkIndex >= streamElement.chunkCount) {\n      // This is beyond the last chunk in the current manifest.\n      out.endOfStream = !manifest.isLive;\n      return;\n    }\n\n    long bufferedDurationUs = loadPositionUs - playbackPositionUs;\n    long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);\n\n    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];\n    for (int i = 0; i < chunkIterators.length; i++) {\n      int trackIndex = trackSelection.getIndexInTrackGroup(i);\n      chunkIterators[i] = new StreamElementIterator(streamElement, trackIndex, chunkIndex);\n    }\n    trackSelection.updateSelectedTrack(\n        playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);\n\n    long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);\n    long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);\n    long chunkSeekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;\n    int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;\n\n    int trackSelectionIndex = trackSelection.getSelectedIndex();\n    ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackSelectionIndex];\n\n    int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);\n    Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);\n\n    out.chunk =\n        newMediaChunk(\n            trackSelection.getSelectedFormat(),\n            dataSource,\n            uri,\n            null,\n            currentAbsoluteChunkIndex,\n            chunkStartTimeUs,\n            chunkEndTimeUs,\n            chunkSeekTimeUs,\n            trackSelection.getSelectionReason(),\n            trackSelection.getSelectionData(),\n            extractorWrapper);\n  }\n\n  @Override\n  public void onChunkLoadCompleted(Chunk chunk) {\n    // Do nothing.\n  }\n\n  @Override\n  public boolean onChunkLoadError(\n      Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) {\n    return cancelable\n        && blacklistDurationMs != C.TIME_UNSET\n        && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), blacklistDurationMs);\n  }\n\n  // Private methods.\n\n  private static MediaChunk newMediaChunk(\n      Format format,\n      DataSource dataSource,\n      Uri uri,\n      String cacheKey,\n      int chunkIndex,\n      long chunkStartTimeUs,\n      long chunkEndTimeUs,\n      long chunkSeekTimeUs,\n      int trackSelectionReason,\n      Object trackSelectionData,\n      ChunkExtractorWrapper extractorWrapper) {\n    DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey);\n    // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.\n    // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.\n    long sampleOffsetUs = chunkStartTimeUs;\n    return new ContainerMediaChunk(\n        dataSource,\n        dataSpec,\n        format,\n        trackSelectionReason,\n        trackSelectionData,\n        chunkStartTimeUs,\n        chunkEndTimeUs,\n        chunkSeekTimeUs,\n        /* clippedEndTimeUs= */ C.TIME_UNSET,\n        chunkIndex,\n        /* chunkCount= */ 1,\n        sampleOffsetUs,\n        extractorWrapper);\n  }\n\n  private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {\n    if (!manifest.isLive) {\n      return C.TIME_UNSET;\n    }\n\n    StreamElement currentElement = manifest.streamElements[streamElementIndex];\n    int lastChunkIndex = currentElement.chunkCount - 1;\n    long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex)\n        + currentElement.getChunkDurationUs(lastChunkIndex);\n    return lastChunkEndTimeUs - playbackPositionUs;\n  }\n\n  /** {@link MediaChunkIterator} wrapping a track of a {@link StreamElement}. */\n  private static final class StreamElementIterator extends BaseMediaChunkIterator {\n\n    private final StreamElement streamElement;\n    private final int trackIndex;\n\n    /**\n     * Creates iterator.\n     *\n     * @param streamElement The {@link StreamElement} to wrap.\n     * @param trackIndex The track index in the stream element.\n     * @param chunkIndex The index of the first available chunk.\n     */\n    public StreamElementIterator(StreamElement streamElement, int trackIndex, int chunkIndex) {\n      super(/* fromIndex= */ chunkIndex, /* toIndex= */ streamElement.chunkCount - 1);\n      this.streamElement = streamElement;\n      this.trackIndex = trackIndex;\n    }\n\n    @Override\n    public DataSpec getDataSpec() {\n      checkInBounds();\n      Uri uri = streamElement.buildRequestUri(trackIndex, (int) getCurrentIndex());\n      return new DataSpec(uri);\n    }\n\n    @Override\n    public long getChunkStartTimeUs() {\n      checkInBounds();\n      return streamElement.getStartTimeUs((int) getCurrentIndex());\n    }\n\n    @Override\n    public long getChunkEndTimeUs() {\n      long chunkStartTimeUs = getChunkStartTimeUs();\n      return chunkStartTimeUs + streamElement.getChunkDurationUs((int) getCurrentIndex());\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.chunk.ChunkSource;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\n\n/**\n * A {@link ChunkSource} for SmoothStreaming.\n */\npublic interface SsChunkSource extends ChunkSource {\n\n  /** Factory for {@link SsChunkSource}s. */\n  interface Factory {\n\n    /**\n     * Creates a new {@link SsChunkSource}.\n     *\n     * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.\n     * @param manifest The initial manifest.\n     * @param streamElementIndex The index of the corresponding stream element in the manifest.\n     * @param trackSelection The track selection.\n     * @param transferListener The transfer listener which should be informed of any data transfers.\n     *     May be null if no listener is available.\n     * @return The created {@link SsChunkSource}.\n     */\n    SsChunkSource createChunkSource(\n        LoaderErrorThrower manifestLoaderErrorThrower,\n        SsManifest manifest,\n        int streamElementIndex,\n        TrackSelection trackSelection,\n        @Nullable TransferListener transferListener);\n  }\n\n  /**\n   * Updates the manifest.\n   *\n   * @param newManifest The new manifest.\n   */\n  void updateManifest(SsManifest newManifest);\n\n  /**\n   * Updates the track selection.\n   *\n   * @param trackSelection The new track selection instance. Must be equivalent to the previous one.\n   */\n  void updateTrackSelection(TrackSelection trackSelection);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** A SmoothStreaming {@link MediaPeriod}. */\n/* package */ final class SsMediaPeriod\n    implements MediaPeriod, SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {\n\n  private final SsChunkSource.Factory chunkSourceFactory;\n  @Nullable private final TransferListener transferListener;\n  private final LoaderErrorThrower manifestLoaderErrorThrower;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final EventDispatcher eventDispatcher;\n  private final Allocator allocator;\n  private final TrackGroupArray trackGroups;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n\n  @Nullable private Callback callback;\n  private SsManifest manifest;\n  private ChunkSampleStream<SsChunkSource>[] sampleStreams;\n  private SequenceableLoader compositeSequenceableLoader;\n  private boolean notifiedReadingStarted;\n\n  public SsMediaPeriod(\n      SsManifest manifest,\n      SsChunkSource.Factory chunkSourceFactory,\n      @Nullable TransferListener transferListener,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      LoaderErrorThrower manifestLoaderErrorThrower,\n      Allocator allocator) {\n    this.manifest = manifest;\n    this.chunkSourceFactory = chunkSourceFactory;\n    this.transferListener = transferListener;\n    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.allocator = allocator;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    trackGroups = buildTrackGroups(manifest, drmSessionManager);\n    sampleStreams = newSampleStreamArray(0);\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  public void updateManifest(SsManifest manifest) {\n    this.manifest = manifest;\n    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {\n      sampleStream.getChunkSource().updateManifest(manifest);\n    }\n    callback.onContinueLoadingRequested(this);\n  }\n\n  public void release() {\n    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {\n      sampleStream.release();\n    }\n    callback = null;\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  // MediaPeriod implementation.\n\n  @Override\n  public void prepare(Callback callback, long positionUs) {\n    this.callback = callback;\n    callback.onPrepared(this);\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    manifestLoaderErrorThrower.maybeThrowError();\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    return trackGroups;\n  }\n\n  @Override\n  public long selectTracks(\n      @NullableType TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      @NullableType SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    ArrayList<ChunkSampleStream<SsChunkSource>> sampleStreamsList = new ArrayList<>();\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] != null) {\n        @SuppressWarnings(\"unchecked\")\n        ChunkSampleStream<SsChunkSource> stream = (ChunkSampleStream<SsChunkSource>) streams[i];\n        if (selections[i] == null || !mayRetainStreamFlags[i]) {\n          stream.release();\n          streams[i] = null;\n        } else {\n          stream.getChunkSource().updateTrackSelection(selections[i]);\n          sampleStreamsList.add(stream);\n        }\n      }\n      if (streams[i] == null && selections[i] != null) {\n        ChunkSampleStream<SsChunkSource> stream = buildSampleStream(selections[i], positionUs);\n        sampleStreamsList.add(stream);\n        streams[i] = stream;\n        streamResetFlags[i] = true;\n      }\n    }\n    sampleStreams = newSampleStreamArray(sampleStreamsList.size());\n    sampleStreamsList.toArray(sampleStreams);\n    compositeSequenceableLoader =\n        compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\n    return positionUs;\n  }\n\n  @Override\n  public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {\n    List<StreamKey> streamKeys = new ArrayList<>();\n    for (int selectionIndex = 0; selectionIndex < trackSelections.size(); selectionIndex++) {\n      TrackSelection trackSelection = trackSelections.get(selectionIndex);\n      int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup());\n      for (int i = 0; i < trackSelection.length(); i++) {\n        streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i)));\n      }\n    }\n    return streamKeys;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {\n      sampleStream.discardBuffer(positionUs, toKeyframe);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    compositeSequenceableLoader.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    return compositeSequenceableLoader.continueLoading(positionUs);\n  }\n\n  @Override\n  public boolean isLoading() {\n    return compositeSequenceableLoader.isLoading();\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return compositeSequenceableLoader.getNextLoadPositionUs();\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    return C.TIME_UNSET;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return compositeSequenceableLoader.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {\n      sampleStream.seekToUs(positionUs);\n    }\n    return positionUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {\n      if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {\n        return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);\n      }\n    }\n    return positionUs;\n  }\n\n  // SequenceableLoader.Callback implementation.\n\n  @Override\n  public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  // Private methods.\n\n  private ChunkSampleStream<SsChunkSource> buildSampleStream(TrackSelection selection,\n      long positionUs) {\n    int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup());\n    SsChunkSource chunkSource =\n        chunkSourceFactory.createChunkSource(\n            manifestLoaderErrorThrower,\n            manifest,\n            streamElementIndex,\n            selection,\n            transferListener);\n    return new ChunkSampleStream<>(\n        manifest.streamElements[streamElementIndex].type,\n        null,\n        null,\n        chunkSource,\n        this,\n        allocator,\n        positionUs,\n        drmSessionManager,\n        loadErrorHandlingPolicy,\n        eventDispatcher);\n  }\n\n  private static TrackGroupArray buildTrackGroups(\n      SsManifest manifest, DrmSessionManager<?> drmSessionManager) {\n    TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length];\n    for (int i = 0; i < manifest.streamElements.length; i++) {\n      Format[] manifestFormats = manifest.streamElements[i].formats;\n      Format[] exposedFormats = new Format[manifestFormats.length];\n      for (int j = 0; j < manifestFormats.length; j++) {\n        Format manifestFormat = manifestFormats[j];\n        exposedFormats[j] =\n            manifestFormat.drmInitData != null\n                ? manifestFormat.copyWithExoMediaCryptoType(\n                    drmSessionManager.getExoMediaCryptoType(manifestFormat.drmInitData))\n                : manifestFormat;\n      }\n      trackGroups[i] = new TrackGroup(exposedFormats);\n    }\n    return new TrackGroupArray(trackGroups);\n  }\n\n  // We won't assign the array to a variable that erases the generic type, and then write into it.\n  @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n  private static ChunkSampleStream<SsChunkSource>[] newSampleStreamArray(int length) {\n    return new ChunkSampleStream[length];\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming;\n\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.offline.FilteringManifestParser;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.BaseMediaSource;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.MediaSourceFactory;\nimport com.google.android.exoplayer2.source.SequenceableLoader;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** A SmoothStreaming {@link MediaSource}. */\npublic final class SsMediaSource extends BaseMediaSource\n    implements Loader.Callback<ParsingLoadable<SsManifest>> {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.smoothstreaming\");\n  }\n\n  /** Factory for {@link SsMediaSource}. */\n  public static final class Factory implements MediaSourceFactory {\n\n    private final SsChunkSource.Factory chunkSourceFactory;\n    @Nullable private final DataSource.Factory manifestDataSourceFactory;\n\n    @Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;\n    @Nullable private List<StreamKey> streamKeys;\n    private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n    private DrmSessionManager<?> drmSessionManager;\n    private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private long livePresentationDelayMs;\n    private boolean isCreateCalled;\n    @Nullable private Object tag;\n\n    /**\n     * Creates a new factory for {@link SsMediaSource}s.\n     *\n     * @param dataSourceFactory A factory for {@link DataSource} instances that will be used to load\n     *     manifest and media data.\n     */\n    public Factory(DataSource.Factory dataSourceFactory) {\n      this(new DefaultSsChunkSource.Factory(dataSourceFactory), dataSourceFactory);\n    }\n\n    /**\n     * Creates a new factory for {@link SsMediaSource}s.\n     *\n     * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n     * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n     *     to load (and refresh) the manifest. May be {@code null} if the factory will only ever be\n     *     used to create create media sources with sideloaded manifests via {@link\n     *     #createMediaSource(SsManifest, Handler, MediaSourceEventListener)}.\n     */\n    public Factory(\n        SsChunkSource.Factory chunkSourceFactory,\n        @Nullable DataSource.Factory manifestDataSourceFactory) {\n      this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory);\n      this.manifestDataSourceFactory = manifestDataSourceFactory;\n      drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();\n      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;\n      compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();\n    }\n\n    /**\n     * Sets a tag for the media source which will be published in the {@link Timeline} of the source\n     * as {@link Timeline.Window#tag}.\n     *\n     * @param tag A tag for the media source.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setTag(@Nullable Object tag) {\n      Assertions.checkState(!isCreateCalled);\n      this.tag = tag;\n      return this;\n    }\n\n    /**\n     * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The\n     * default value is {@link DrmSessionManager#DUMMY}.\n     *\n     * @param drmSessionManager The {@link DrmSessionManager}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {\n      Assertions.checkState(!isCreateCalled);\n      this.drmSessionManager = drmSessionManager;\n      return this;\n    }\n\n    /**\n     * Sets the minimum number of times to retry if a loading error occurs. See {@link\n     * #setLoadErrorHandlingPolicy} for the default value.\n     *\n     * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\n     * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\n     * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\n     *\n     * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\n     */\n    @Deprecated\n    public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\n      return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));\n    }\n\n    /**\n     * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\n     * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\n     *\n     * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\n     *\n     * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\n      Assertions.checkState(!isCreateCalled);\n      this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n      return this;\n    }\n\n    /**\n     * Sets the duration in milliseconds by which the default start position should precede the end\n     * of the live window for live playbacks. The default value is {@link\n     * #DEFAULT_LIVE_PRESENTATION_DELAY_MS}.\n     *\n     * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n     *     default start position should precede the end of the live window.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setLivePresentationDelayMs(long livePresentationDelayMs) {\n      Assertions.checkState(!isCreateCalled);\n      this.livePresentationDelayMs = livePresentationDelayMs;\n      return this;\n    }\n\n    /**\n     * Sets the manifest parser to parse loaded manifest data when loading a manifest URI.\n     *\n     * @param manifestParser A parser for loaded manifest data.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setManifestParser(ParsingLoadable.Parser<? extends SsManifest> manifestParser) {\n      Assertions.checkState(!isCreateCalled);\n      this.manifestParser = Assertions.checkNotNull(manifestParser);\n      return this;\n    }\n\n    /**\n     * Sets the factory to create composite {@link SequenceableLoader}s for when this media source\n     * loads data from multiple streams (video, audio etc.). The default is an instance of {@link\n     * DefaultCompositeSequenceableLoaderFactory}.\n     *\n     * @param compositeSequenceableLoaderFactory A factory to create composite {@link\n     *     SequenceableLoader}s for when this media source loads data from multiple streams (video,\n     *     audio etc.).\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setCompositeSequenceableLoaderFactory(\n        CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {\n      Assertions.checkState(!isCreateCalled);\n      this.compositeSequenceableLoaderFactory =\n          Assertions.checkNotNull(compositeSequenceableLoaderFactory);\n      return this;\n    }\n\n    /**\n     * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded\n     * manifest.\n     *\n     * @param manifest The manifest. {@link SsManifest#isLive} must be false.\n     * @return The new {@link SsMediaSource}.\n     * @throws IllegalArgumentException If {@link SsManifest#isLive} is true.\n     */\n    public SsMediaSource createMediaSource(SsManifest manifest) {\n      Assertions.checkArgument(!manifest.isLive);\n      isCreateCalled = true;\n      if (streamKeys != null && !streamKeys.isEmpty()) {\n        manifest = manifest.copy(streamKeys);\n      }\n      return new SsMediaSource(\n          manifest,\n          /* manifestUri= */ null,\n          /* manifestDataSourceFactory= */ null,\n          /* manifestParser= */ null,\n          chunkSourceFactory,\n          compositeSequenceableLoaderFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          tag);\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(SsManifest)} and {@link #addEventListener(Handler,\n     *     MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public SsMediaSource createMediaSource(\n        SsManifest manifest,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      SsMediaSource mediaSource = createMediaSource(manifest);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    /**\n     * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler,\n     *     MediaSourceEventListener)} instead.\n     */\n    @Deprecated\n    public SsMediaSource createMediaSource(\n        Uri manifestUri,\n        @Nullable Handler eventHandler,\n        @Nullable MediaSourceEventListener eventListener) {\n      SsMediaSource mediaSource = createMediaSource(manifestUri);\n      if (eventHandler != null && eventListener != null) {\n        mediaSource.addEventListener(eventHandler, eventListener);\n      }\n      return mediaSource;\n    }\n\n    /**\n     * Returns a new {@link SsMediaSource} using the current parameters.\n     *\n     * @param manifestUri The manifest {@link Uri}.\n     * @return The new {@link SsMediaSource}.\n     */\n    @Override\n    public SsMediaSource createMediaSource(Uri manifestUri) {\n      isCreateCalled = true;\n      if (manifestParser == null) {\n        manifestParser = new SsManifestParser();\n      }\n      if (streamKeys != null) {\n        manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);\n      }\n      return new SsMediaSource(\n          /* manifest= */ null,\n          Assertions.checkNotNull(manifestUri),\n          manifestDataSourceFactory,\n          manifestParser,\n          chunkSourceFactory,\n          compositeSequenceableLoaderFactory,\n          drmSessionManager,\n          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          tag);\n    }\n\n    @Override\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\n      return this;\n    }\n\n    @Override\n    public int[] getSupportedTypes() {\n      return new int[] {C.TYPE_SS};\n    }\n\n  }\n\n  /**\n   * The default presentation delay for live streams. The presentation delay is the duration by\n   * which the default start position precedes the end of the live window.\n   */\n  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000;\n\n  /**\n   * The minimum period between manifest refreshes.\n   */\n  private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;\n  /**\n   * The minimum default start position for live streams, relative to the start of the live window.\n   */\n  private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000;\n\n  private final boolean sideloadedManifest;\n  private final Uri manifestUri;\n  private final DataSource.Factory manifestDataSourceFactory;\n  private final SsChunkSource.Factory chunkSourceFactory;\n  private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\n  private final DrmSessionManager<?> drmSessionManager;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final long livePresentationDelayMs;\n  private final EventDispatcher manifestEventDispatcher;\n  private final ParsingLoadable.Parser<? extends SsManifest> manifestParser;\n  private final ArrayList<SsMediaPeriod> mediaPeriods;\n  @Nullable private final Object tag;\n\n  private DataSource manifestDataSource;\n  private Loader manifestLoader;\n  private LoaderErrorThrower manifestLoaderErrorThrower;\n  @Nullable private TransferListener mediaTransferListener;\n\n  private long manifestLoadStartTimestamp;\n  private SsManifest manifest;\n\n  private Handler manifestRefreshHandler;\n\n  /**\n   * Constructs an instance to play a given {@link SsManifest}, which must not be live.\n   *\n   * @param manifest The manifest. {@link SsManifest#isLive} must be false.\n   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SsMediaSource(\n      SsManifest manifest,\n      SsChunkSource.Factory chunkSourceFactory,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        chunkSourceFactory,\n        DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * Constructs an instance to play a given {@link SsManifest}, which must not be live.\n   *\n   * @param manifest The manifest. {@link SsManifest#isLive} must be false.\n   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public SsMediaSource(\n      SsManifest manifest,\n      SsChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        /* manifestUri= */ null,\n        /* manifestDataSourceFactory= */ null,\n        /* manifestParser= */ null,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\n        DrmSessionManager.getDummyDrmSessionManager(),\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        DEFAULT_LIVE_PRESENTATION_DELAY_MS,\n        /* tag= */ null);\n    if (eventHandler != null && eventListener != null) {\n      addEventListener(eventHandler, eventListener);\n    }\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or\n   * on-demand.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SsMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      SsChunkSource.Factory chunkSourceFactory,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        manifestUri,\n        manifestDataSourceFactory,\n        chunkSourceFactory,\n        DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT,\n        DEFAULT_LIVE_PRESENTATION_DELAY_MS,\n        eventHandler,\n        eventListener);\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or\n   * on-demand.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n   *     default start position should precede the end of the live window.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SsMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      SsChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      long livePresentationDelayMs,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory,\n        minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener);\n  }\n\n  /**\n   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or\n   * on-demand.\n   *\n   * @param manifestUri The manifest {@link Uri}.\n   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\n   *     to load (and refresh) the manifest.\n   * @param manifestParser A parser for loaded manifest data.\n   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.\n   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\n   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\n   *     default start position should precede the end of the live window.\n   * @param eventHandler A handler for events. May be null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @deprecated Use {@link Factory} instead.\n   */\n  @Deprecated\n  public SsMediaSource(\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      ParsingLoadable.Parser<? extends SsManifest> manifestParser,\n      SsChunkSource.Factory chunkSourceFactory,\n      int minLoadableRetryCount,\n      long livePresentationDelayMs,\n      @Nullable Handler eventHandler,\n      @Nullable MediaSourceEventListener eventListener) {\n    this(\n        /* manifest= */ null,\n        manifestUri,\n        manifestDataSourceFactory,\n        manifestParser,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\n        DrmSessionManager.getDummyDrmSessionManager(),\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        livePresentationDelayMs,\n        /* tag= */ null);\n    if (eventHandler != null && eventListener != null) {\n      addEventListener(eventHandler, eventListener);\n    }\n  }\n\n  private SsMediaSource(\n      @Nullable SsManifest manifest,\n      @Nullable Uri manifestUri,\n      @Nullable DataSource.Factory manifestDataSourceFactory,\n      @Nullable ParsingLoadable.Parser<? extends SsManifest> manifestParser,\n      SsChunkSource.Factory chunkSourceFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      DrmSessionManager<?> drmSessionManager,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      long livePresentationDelayMs,\n      @Nullable Object tag) {\n    Assertions.checkState(manifest == null || !manifest.isLive);\n    this.manifest = manifest;\n    this.manifestUri = manifestUri == null ? null : SsUtil.fixManifestUri(manifestUri);\n    this.manifestDataSourceFactory = manifestDataSourceFactory;\n    this.manifestParser = manifestParser;\n    this.chunkSourceFactory = chunkSourceFactory;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    this.drmSessionManager = drmSessionManager;\n    this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.livePresentationDelayMs = livePresentationDelayMs;\n    this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);\n    this.tag = tag;\n    sideloadedManifest = manifest != null;\n    mediaPeriods = new ArrayList<>();\n  }\n\n  // MediaSource implementation.\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\n    drmSessionManager.prepare();\n    if (sideloadedManifest) {\n      manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy();\n      processManifest();\n    } else {\n      manifestDataSource = manifestDataSourceFactory.createDataSource();\n      manifestLoader = new Loader(\"Loader:Manifest\");\n      manifestLoaderErrorThrower = manifestLoader;\n      manifestRefreshHandler = new Handler();\n      startLoadingManifest();\n    }\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    manifestLoaderErrorThrower.maybeThrowError();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    EventDispatcher eventDispatcher = createEventDispatcher(id);\n    SsMediaPeriod period =\n        new SsMediaPeriod(\n            manifest,\n            chunkSourceFactory,\n            mediaTransferListener,\n            compositeSequenceableLoaderFactory,\n            drmSessionManager,\n            loadErrorHandlingPolicy,\n            eventDispatcher,\n            manifestLoaderErrorThrower,\n            allocator);\n    mediaPeriods.add(period);\n    return period;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod period) {\n    ((SsMediaPeriod) period).release();\n    mediaPeriods.remove(period);\n  }\n\n  @Override\n  protected void releaseSourceInternal() {\n    manifest = sideloadedManifest ? manifest : null;\n    manifestDataSource = null;\n    manifestLoadStartTimestamp = 0;\n    if (manifestLoader != null) {\n      manifestLoader.release();\n      manifestLoader = null;\n    }\n    if (manifestRefreshHandler != null) {\n      manifestRefreshHandler.removeCallbacksAndMessages(null);\n      manifestRefreshHandler = null;\n    }\n    drmSessionManager.release();\n  }\n\n  // Loader.Callback implementation\n\n  @Override\n  public void onLoadCompleted(ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs,\n      long loadDurationMs) {\n    manifestEventDispatcher.loadCompleted(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n    manifest = loadable.getResult();\n    manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;\n    processManifest();\n    scheduleManifestRefresh();\n  }\n\n  @Override\n  public void onLoadCanceled(ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs,\n      long loadDurationMs, boolean released) {\n    manifestEventDispatcher.loadCanceled(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded());\n  }\n\n  @Override\n  public LoadErrorAction onLoadError(\n      ParsingLoadable<SsManifest> loadable,\n      long elapsedRealtimeMs,\n      long loadDurationMs,\n      IOException error,\n      int errorCount) {\n    long retryDelayMs =\n        loadErrorHandlingPolicy.getRetryDelayMsFor(\n            C.DATA_TYPE_MANIFEST, loadDurationMs, error, errorCount);\n    LoadErrorAction loadErrorAction =\n        retryDelayMs == C.TIME_UNSET\n            ? Loader.DONT_RETRY_FATAL\n            : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);\n    manifestEventDispatcher.loadError(\n        loadable.dataSpec,\n        loadable.getUri(),\n        loadable.getResponseHeaders(),\n        loadable.type,\n        elapsedRealtimeMs,\n        loadDurationMs,\n        loadable.bytesLoaded(),\n        error,\n        !loadErrorAction.isRetry());\n    return loadErrorAction;\n  }\n\n  // Internal methods\n\n  private void processManifest() {\n    for (int i = 0; i < mediaPeriods.size(); i++) {\n      mediaPeriods.get(i).updateManifest(manifest);\n    }\n\n    long startTimeUs = Long.MAX_VALUE;\n    long endTimeUs = Long.MIN_VALUE;\n    for (StreamElement element : manifest.streamElements) {\n      if (element.chunkCount > 0) {\n        startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0));\n        endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1)\n            + element.getChunkDurationUs(element.chunkCount - 1));\n      }\n    }\n\n    Timeline timeline;\n    if (startTimeUs == Long.MAX_VALUE) {\n      long periodDurationUs = manifest.isLive ? C.TIME_UNSET : 0;\n      timeline =\n          new SinglePeriodTimeline(\n              periodDurationUs,\n              /* windowDurationUs= */ 0,\n              /* windowPositionInPeriodUs= */ 0,\n              /* windowDefaultStartPositionUs= */ 0,\n              /* isSeekable= */ true,\n              /* isDynamic= */ manifest.isLive,\n              /* isLive= */ manifest.isLive,\n              manifest,\n              tag);\n    } else if (manifest.isLive) {\n      if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) {\n        startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs);\n      }\n      long durationUs = endTimeUs - startTimeUs;\n      long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs);\n      if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {\n        // The default start position is too close to the start of the live window. Set it to the\n        // minimum default start position provided the window is at least twice as big. Else set\n        // it to the middle of the window.\n        defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2);\n      }\n      timeline =\n          new SinglePeriodTimeline(\n              /* periodDurationUs= */ C.TIME_UNSET,\n              durationUs,\n              startTimeUs,\n              defaultStartPositionUs,\n              /* isSeekable= */ true,\n              /* isDynamic= */ true,\n              /* isLive= */ true,\n              manifest,\n              tag);\n    } else {\n      long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs\n          : endTimeUs - startTimeUs;\n      timeline =\n          new SinglePeriodTimeline(\n              startTimeUs + durationUs,\n              durationUs,\n              startTimeUs,\n              /* windowDefaultStartPositionUs= */ 0,\n              /* isSeekable= */ true,\n              /* isDynamic= */ false,\n              /* isLive= */ false,\n              manifest,\n              tag);\n    }\n    refreshSourceInfo(timeline);\n  }\n\n  private void scheduleManifestRefresh() {\n    if (!manifest.isLive) {\n      return;\n    }\n    long nextLoadTimestamp = manifestLoadStartTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS;\n    long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime());\n    manifestRefreshHandler.postDelayed(this::startLoadingManifest, delayUntilNextLoad);\n  }\n\n  private void startLoadingManifest() {\n    if (manifestLoader.hasFatalError()) {\n      return;\n    }\n    ParsingLoadable<SsManifest> loadable = new ParsingLoadable<>(manifestDataSource,\n        manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);\n    long elapsedRealtimeMs =\n        manifestLoader.startLoading(\n            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));\n    manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming.manifest;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;\nimport com.google.android.exoplayer2.offline.FilterableManifest;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.UriUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * Represents a SmoothStreaming manifest.\n *\n * @see <a href=\"http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx\">IIS Smooth\n *     Streaming Client Manifest Format</a>\n */\npublic class SsManifest implements FilterableManifest<SsManifest> {\n\n  /** Represents a protection element containing a single header. */\n  public static class ProtectionElement {\n\n    public final UUID uuid;\n    public final byte[] data;\n    public final TrackEncryptionBox[] trackEncryptionBoxes;\n\n    public ProtectionElement(UUID uuid, byte[] data, TrackEncryptionBox[] trackEncryptionBoxes) {\n      this.uuid = uuid;\n      this.data = data;\n      this.trackEncryptionBoxes = trackEncryptionBoxes;\n    }\n  }\n\n  /**\n   * Represents a StreamIndex element.\n   */\n  public static class StreamElement {\n\n    private static final String URL_PLACEHOLDER_START_TIME_1 = \"{start time}\";\n    private static final String URL_PLACEHOLDER_START_TIME_2 = \"{start_time}\";\n    private static final String URL_PLACEHOLDER_BITRATE_1 = \"{bitrate}\";\n    private static final String URL_PLACEHOLDER_BITRATE_2 = \"{Bitrate}\";\n\n    public final int type;\n    public final String subType;\n    public final long timescale;\n    public final String name;\n    public final int maxWidth;\n    public final int maxHeight;\n    public final int displayWidth;\n    public final int displayHeight;\n    @Nullable public final String language;\n    public final Format[] formats;\n    public final int chunkCount;\n\n    private final String baseUri;\n    private final String chunkTemplate;\n\n    private final List<Long> chunkStartTimes;\n    private final long[] chunkStartTimesUs;\n    private final long lastChunkDurationUs;\n\n    public StreamElement(\n        String baseUri,\n        String chunkTemplate,\n        int type,\n        String subType,\n        long timescale,\n        String name,\n        int maxWidth,\n        int maxHeight,\n        int displayWidth,\n        int displayHeight,\n        @Nullable String language,\n        Format[] formats,\n        List<Long> chunkStartTimes,\n        long lastChunkDuration) {\n      this(\n          baseUri,\n          chunkTemplate,\n          type,\n          subType,\n          timescale,\n          name,\n          maxWidth,\n          maxHeight,\n          displayWidth,\n          displayHeight,\n          language,\n          formats,\n          chunkStartTimes,\n          Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale),\n          Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale));\n    }\n\n    private StreamElement(\n        String baseUri,\n        String chunkTemplate,\n        int type,\n        String subType,\n        long timescale,\n        String name,\n        int maxWidth,\n        int maxHeight,\n        int displayWidth,\n        int displayHeight,\n        @Nullable String language,\n        Format[] formats,\n        List<Long> chunkStartTimes,\n        long[] chunkStartTimesUs,\n        long lastChunkDurationUs) {\n      this.baseUri = baseUri;\n      this.chunkTemplate = chunkTemplate;\n      this.type = type;\n      this.subType = subType;\n      this.timescale = timescale;\n      this.name = name;\n      this.maxWidth = maxWidth;\n      this.maxHeight = maxHeight;\n      this.displayWidth = displayWidth;\n      this.displayHeight = displayHeight;\n      this.language = language;\n      this.formats = formats;\n      this.chunkStartTimes = chunkStartTimes;\n      this.chunkStartTimesUs = chunkStartTimesUs;\n      this.lastChunkDurationUs = lastChunkDurationUs;\n      chunkCount = chunkStartTimes.size();\n    }\n\n    /**\n     * Creates a copy of this stream element with the formats replaced with those specified.\n     *\n     * @param formats The formats to be included in the copy.\n     * @return A copy of this stream element with the formats replaced.\n     * @throws IndexOutOfBoundsException If a key has an invalid index.\n     */\n    public StreamElement copy(Format[] formats) {\n      return new StreamElement(baseUri, chunkTemplate, type, subType, timescale, name, maxWidth,\n          maxHeight, displayWidth, displayHeight, language, formats, chunkStartTimes,\n          chunkStartTimesUs, lastChunkDurationUs);\n    }\n\n    /**\n     * Returns the index of the chunk that contains the specified time.\n     *\n     * @param timeUs The time in microseconds.\n     * @return The index of the corresponding chunk.\n     */\n    public int getChunkIndex(long timeUs) {\n      return Util.binarySearchFloor(chunkStartTimesUs, timeUs, true, true);\n    }\n\n    /**\n     * Returns the start time of the specified chunk.\n     *\n     * @param chunkIndex The index of the chunk.\n     * @return The start time of the chunk, in microseconds.\n     */\n    public long getStartTimeUs(int chunkIndex) {\n      return chunkStartTimesUs[chunkIndex];\n    }\n\n    /**\n     * Returns the duration of the specified chunk.\n     *\n     * @param chunkIndex The index of the chunk.\n     * @return The duration of the chunk, in microseconds.\n     */\n    public long getChunkDurationUs(int chunkIndex) {\n      return (chunkIndex == chunkCount - 1) ? lastChunkDurationUs\n          : chunkStartTimesUs[chunkIndex + 1] - chunkStartTimesUs[chunkIndex];\n    }\n\n    /**\n     * Builds a uri for requesting the specified chunk of the specified track.\n     *\n     * @param track The index of the track for which to build the URL.\n     * @param chunkIndex The index of the chunk for which to build the URL.\n     * @return The request uri.\n     */\n    public Uri buildRequestUri(int track, int chunkIndex) {\n      Assertions.checkState(formats != null);\n      Assertions.checkState(chunkStartTimes != null);\n      Assertions.checkState(chunkIndex < chunkStartTimes.size());\n      String bitrateString = Integer.toString(formats[track].bitrate);\n      String startTimeString = chunkStartTimes.get(chunkIndex).toString();\n      String chunkUrl = chunkTemplate\n          .replace(URL_PLACEHOLDER_BITRATE_1, bitrateString)\n          .replace(URL_PLACEHOLDER_BITRATE_2, bitrateString)\n          .replace(URL_PLACEHOLDER_START_TIME_1, startTimeString)\n          .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString);\n      return UriUtil.resolveToUri(baseUri, chunkUrl);\n    }\n  }\n\n  public static final int UNSET_LOOKAHEAD = -1;\n\n  /** The client manifest major version. */\n  public final int majorVersion;\n\n  /** The client manifest minor version. */\n  public final int minorVersion;\n\n  /**\n   * The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if the lookahead is\n   * unspecified.\n   */\n  public final int lookAheadCount;\n\n  /** Whether the manifest describes a live presentation still in progress. */\n  public final boolean isLive;\n\n  /** Content protection information, or null if the content is not protected. */\n  @Nullable public final ProtectionElement protectionElement;\n\n  /** The contained stream elements. */\n  public final StreamElement[] streamElements;\n\n  /**\n   * The overall presentation duration of the media in microseconds, or {@link C#TIME_UNSET} if the\n   * duration is unknown.\n   */\n  public final long durationUs;\n\n  /**\n   * The length of the trailing window for a live broadcast in microseconds, or {@link C#TIME_UNSET}\n   * if the stream is not live or if the window length is unspecified.\n   */\n  public final long dvrWindowLengthUs;\n\n  /**\n   * @param majorVersion The client manifest major version.\n   * @param minorVersion The client manifest minor version.\n   * @param timescale The timescale of the media as the number of units that pass in one second.\n   * @param duration The overall presentation duration in units of the timescale attribute, or 0 if\n   *     the duration is unknown.\n   * @param dvrWindowLength The length of the trailing window in units of the timescale attribute,\n   *     or 0 if this attribute is unspecified or not applicable.\n   * @param lookAheadCount The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if\n   *     this attribute is unspecified or not applicable.\n   * @param isLive True if the manifest describes a live presentation still in progress. False\n   *     otherwise.\n   * @param protectionElement Content protection information, or null if the content is not\n   *     protected.\n   * @param streamElements The contained stream elements.\n   */\n  public SsManifest(\n      int majorVersion,\n      int minorVersion,\n      long timescale,\n      long duration,\n      long dvrWindowLength,\n      int lookAheadCount,\n      boolean isLive,\n      @Nullable ProtectionElement protectionElement,\n      StreamElement[] streamElements) {\n    this(\n        majorVersion,\n        minorVersion,\n        duration == 0\n            ? C.TIME_UNSET\n            : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale),\n        dvrWindowLength == 0\n            ? C.TIME_UNSET\n            : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale),\n        lookAheadCount,\n        isLive,\n        protectionElement,\n        streamElements);\n  }\n\n  private SsManifest(\n      int majorVersion,\n      int minorVersion,\n      long durationUs,\n      long dvrWindowLengthUs,\n      int lookAheadCount,\n      boolean isLive,\n      @Nullable ProtectionElement protectionElement,\n      StreamElement[] streamElements) {\n    this.majorVersion = majorVersion;\n    this.minorVersion = minorVersion;\n    this.durationUs = durationUs;\n    this.dvrWindowLengthUs = dvrWindowLengthUs;\n    this.lookAheadCount = lookAheadCount;\n    this.isLive = isLive;\n    this.protectionElement = protectionElement;\n    this.streamElements = streamElements;\n  }\n\n  @Override\n  public final SsManifest copy(List<StreamKey> streamKeys) {\n    ArrayList<StreamKey> sortedKeys = new ArrayList<>(streamKeys);\n    Collections.sort(sortedKeys);\n\n    StreamElement currentStreamElement = null;\n    List<StreamElement> copiedStreamElements = new ArrayList<>();\n    List<Format> copiedFormats = new ArrayList<>();\n    for (int i = 0; i < sortedKeys.size(); i++) {\n      StreamKey key = sortedKeys.get(i);\n      StreamElement streamElement = streamElements[key.groupIndex];\n      if (streamElement != currentStreamElement && currentStreamElement != null) {\n        // We're advancing to a new stream element. Add the current one.\n        copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0])));\n        copiedFormats.clear();\n      }\n      currentStreamElement = streamElement;\n      copiedFormats.add(streamElement.formats[key.trackIndex]);\n    }\n    if (currentStreamElement != null) {\n      // Add the last stream element.\n      copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0])));\n    }\n\n    StreamElement[] copiedStreamElementsArray = copiedStreamElements.toArray(new StreamElement[0]);\n    return new SsManifest(\n        majorVersion,\n        minorVersion,\n        durationUs,\n        dvrWindowLengthUs,\n        lookAheadCount,\n        isLive,\n        protectionElement,\n        copiedStreamElementsArray);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming.manifest;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;\nimport com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.UUID;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\nimport org.xmlpull.v1.XmlPullParserFactory;\n\n/**\n * Parses SmoothStreaming client manifests.\n *\n * @see <a href=\"http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx\">\n * IIS Smooth Streaming Client Manifest Format</a>\n */\npublic class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {\n\n  private final XmlPullParserFactory xmlParserFactory;\n\n  public SsManifestParser() {\n    try {\n      xmlParserFactory = XmlPullParserFactory.newInstance();\n    } catch (XmlPullParserException e) {\n      throw new RuntimeException(\"Couldn't create XmlPullParserFactory instance\", e);\n    }\n  }\n\n  @Override\n  public SsManifest parse(Uri uri, InputStream inputStream) throws IOException {\n    try {\n      XmlPullParser xmlParser = xmlParserFactory.newPullParser();\n      xmlParser.setInput(inputStream, null);\n      SmoothStreamingMediaParser smoothStreamingMediaParser =\n          new SmoothStreamingMediaParser(null, uri.toString());\n      return (SsManifest) smoothStreamingMediaParser.parse(xmlParser);\n    } catch (XmlPullParserException e) {\n      throw new ParserException(e);\n    }\n  }\n\n  /**\n   * Thrown if a required field is missing.\n   */\n  public static class MissingFieldException extends ParserException {\n\n    public MissingFieldException(String fieldName) {\n      super(\"Missing required field: \" + fieldName);\n    }\n\n  }\n\n  /**\n   * A base class for parsers that parse components of a smooth streaming manifest.\n   */\n  private abstract static class ElementParser {\n\n    private final String baseUri;\n    private final String tag;\n\n    @Nullable private final ElementParser parent;\n    private final List<Pair<String, @NullableType Object>> normalizedAttributes;\n\n    public ElementParser(@Nullable ElementParser parent, String baseUri, String tag) {\n      this.parent = parent;\n      this.baseUri = baseUri;\n      this.tag = tag;\n      this.normalizedAttributes = new LinkedList<>();\n    }\n\n    public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException {\n      String tagName;\n      boolean foundStartTag = false;\n      int skippingElementDepth = 0;\n      while (true) {\n        int eventType = xmlParser.getEventType();\n        switch (eventType) {\n          case XmlPullParser.START_TAG:\n            tagName = xmlParser.getName();\n            if (tag.equals(tagName)) {\n              foundStartTag = true;\n              parseStartTag(xmlParser);\n            } else if (foundStartTag) {\n              if (skippingElementDepth > 0) {\n                skippingElementDepth++;\n              } else if (handleChildInline(tagName)) {\n                parseStartTag(xmlParser);\n              } else {\n                ElementParser childElementParser = newChildParser(this, tagName, baseUri);\n                if (childElementParser == null) {\n                  skippingElementDepth = 1;\n                } else {\n                  addChild(childElementParser.parse(xmlParser));\n                }\n              }\n            }\n            break;\n          case XmlPullParser.TEXT:\n            if (foundStartTag && skippingElementDepth == 0) {\n              parseText(xmlParser);\n            }\n            break;\n          case XmlPullParser.END_TAG:\n            if (foundStartTag) {\n              if (skippingElementDepth > 0) {\n                skippingElementDepth--;\n              } else {\n                tagName = xmlParser.getName();\n                parseEndTag(xmlParser);\n                if (!handleChildInline(tagName)) {\n                  return build();\n                }\n              }\n            }\n            break;\n          case XmlPullParser.END_DOCUMENT:\n            return null;\n          default:\n            // Do nothing.\n            break;\n        }\n        xmlParser.next();\n      }\n    }\n\n    private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {\n      if (QualityLevelParser.TAG.equals(name)) {\n        return new QualityLevelParser(parent, baseUri);\n      } else if (ProtectionParser.TAG.equals(name)) {\n        return new ProtectionParser(parent, baseUri);\n      } else if (StreamIndexParser.TAG.equals(name)) {\n        return new StreamIndexParser(parent, baseUri);\n      }\n      return null;\n    }\n\n    /**\n     * Stash an attribute that may be normalized at this level. In other words, an attribute that\n     * may have been pulled up from the child elements because its value was the same in all\n     * children.\n     *\n     * <p>Stashing an attribute allows child element parsers to retrieve the values of normalized\n     * attributes using {@link #getNormalizedAttribute(String)}.\n     *\n     * @param key The name of the attribute.\n     * @param value The value of the attribute.\n     */\n    protected final void putNormalizedAttribute(String key, @Nullable Object value) {\n      normalizedAttributes.add(Pair.create(key, value));\n    }\n\n    /**\n     * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with the\n     * provided name, the parent element parser will be queried, and so on up the chain.\n     *\n     * @param key The name of the attribute.\n     * @return The stashed value, or null if the attribute was not be found.\n     */\n    @Nullable\n    protected final Object getNormalizedAttribute(String key) {\n      for (int i = 0; i < normalizedAttributes.size(); i++) {\n        Pair<String, Object> pair = normalizedAttributes.get(i);\n        if (pair.first.equals(key)) {\n          return pair.second;\n        }\n      }\n      return parent == null ? null : parent.getNormalizedAttribute(key);\n    }\n\n    /**\n     * Whether this {@link ElementParser} parses a child element inline.\n     *\n     * @param tagName The name of the child element.\n     * @return Whether the child is parsed inline.\n     */\n    protected boolean handleChildInline(String tagName) {\n      return false;\n    }\n\n    /**\n     * @param xmlParser The underlying {@link XmlPullParser}\n     * @throws ParserException If a parsing error occurs.\n     */\n    protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {\n      // Do nothing.\n    }\n\n    /**\n     * @param xmlParser The underlying {@link XmlPullParser}\n     */\n    protected void parseText(XmlPullParser xmlParser) {\n      // Do nothing.\n    }\n\n    /**\n     * @param xmlParser The underlying {@link XmlPullParser}\n     */\n    protected void parseEndTag(XmlPullParser xmlParser) {\n      // Do nothing.\n    }\n\n    /**\n     * @param parsedChild A parsed child object.\n     */\n    protected void addChild(Object parsedChild) {\n      // Do nothing.\n    }\n\n    protected abstract Object build();\n\n    protected final String parseRequiredString(XmlPullParser parser, String key)\n        throws MissingFieldException {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        return value;\n      } else {\n        throw new MissingFieldException(key);\n      }\n    }\n\n    protected final int parseInt(XmlPullParser parser, String key, int defaultValue)\n        throws ParserException {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        try {\n          return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n          throw new ParserException(e);\n        }\n      } else {\n        return defaultValue;\n      }\n    }\n\n    protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        try {\n          return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n          throw new ParserException(e);\n        }\n      } else {\n        throw new MissingFieldException(key);\n      }\n    }\n\n    protected final long parseLong(XmlPullParser parser, String key, long defaultValue)\n        throws ParserException {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        try {\n          return Long.parseLong(value);\n        } catch (NumberFormatException e) {\n          throw new ParserException(e);\n        }\n      } else {\n        return defaultValue;\n      }\n    }\n\n    protected final long parseRequiredLong(XmlPullParser parser, String key)\n        throws ParserException {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        try {\n          return Long.parseLong(value);\n        } catch (NumberFormatException e) {\n          throw new ParserException(e);\n        }\n      } else {\n        throw new MissingFieldException(key);\n      }\n    }\n\n    protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) {\n      String value = parser.getAttributeValue(null, key);\n      if (value != null) {\n        return Boolean.parseBoolean(value);\n      } else {\n        return defaultValue;\n      }\n    }\n\n  }\n\n  private static class SmoothStreamingMediaParser extends ElementParser {\n\n    public static final String TAG = \"SmoothStreamingMedia\";\n\n    private static final String KEY_MAJOR_VERSION = \"MajorVersion\";\n    private static final String KEY_MINOR_VERSION = \"MinorVersion\";\n    private static final String KEY_TIME_SCALE = \"TimeScale\";\n    private static final String KEY_DVR_WINDOW_LENGTH = \"DVRWindowLength\";\n    private static final String KEY_DURATION = \"Duration\";\n    private static final String KEY_LOOKAHEAD_COUNT = \"LookaheadCount\";\n    private static final String KEY_IS_LIVE = \"IsLive\";\n\n    private final List<StreamElement> streamElements;\n\n    private int majorVersion;\n    private int minorVersion;\n    private long timescale;\n    private long duration;\n    private long dvrWindowLength;\n    private int lookAheadCount;\n    private boolean isLive;\n    @Nullable private ProtectionElement protectionElement;\n\n    public SmoothStreamingMediaParser(ElementParser parent, String baseUri) {\n      super(parent, baseUri, TAG);\n      lookAheadCount = SsManifest.UNSET_LOOKAHEAD;\n      protectionElement = null;\n      streamElements = new LinkedList<>();\n    }\n\n    @Override\n    public void parseStartTag(XmlPullParser parser) throws ParserException {\n      majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);\n      minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);\n      timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L);\n      duration = parseRequiredLong(parser, KEY_DURATION);\n      dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);\n      lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, SsManifest.UNSET_LOOKAHEAD);\n      isLive = parseBoolean(parser, KEY_IS_LIVE, false);\n      putNormalizedAttribute(KEY_TIME_SCALE, timescale);\n    }\n\n    @Override\n    public void addChild(Object child) {\n      if (child instanceof StreamElement) {\n        streamElements.add((StreamElement) child);\n      } else if (child instanceof ProtectionElement) {\n        Assertions.checkState(protectionElement == null);\n        protectionElement = (ProtectionElement) child;\n      }\n    }\n\n    @Override\n    public Object build() {\n      StreamElement[] streamElementArray = new StreamElement[streamElements.size()];\n      streamElements.toArray(streamElementArray);\n      if (protectionElement != null) {\n        DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid,\n            MimeTypes.VIDEO_MP4, protectionElement.data));\n        for (StreamElement streamElement : streamElementArray) {\n          int type = streamElement.type;\n          if (type == C.TRACK_TYPE_VIDEO || type == C.TRACK_TYPE_AUDIO) {\n            Format[] formats = streamElement.formats;\n            for (int i = 0; i < formats.length; i++) {\n              formats[i] = formats[i].copyWithDrmInitData(drmInitData);\n            }\n          }\n        }\n      }\n      return new SsManifest(majorVersion, minorVersion, timescale, duration, dvrWindowLength,\n          lookAheadCount, isLive, protectionElement, streamElementArray);\n    }\n\n  }\n\n  private static class ProtectionParser extends ElementParser {\n\n    public static final String TAG = \"Protection\";\n    public static final String TAG_PROTECTION_HEADER = \"ProtectionHeader\";\n    public static final String KEY_SYSTEM_ID = \"SystemID\";\n\n    private static final int INITIALIZATION_VECTOR_SIZE = 8;\n\n    private boolean inProtectionHeader;\n    private UUID uuid;\n    private byte[] initData;\n\n    public ProtectionParser(ElementParser parent, String baseUri) {\n      super(parent, baseUri, TAG);\n    }\n\n    @Override\n    public boolean handleChildInline(String tag) {\n      return TAG_PROTECTION_HEADER.equals(tag);\n    }\n\n    @Override\n    public void parseStartTag(XmlPullParser parser) {\n      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {\n        inProtectionHeader = true;\n        String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);\n        uuidString = stripCurlyBraces(uuidString);\n        uuid = UUID.fromString(uuidString);\n      }\n    }\n\n    @Override\n    public void parseText(XmlPullParser parser) {\n      if (inProtectionHeader) {\n        initData = Base64.decode(parser.getText(), Base64.DEFAULT);\n      }\n    }\n\n    @Override\n    public void parseEndTag(XmlPullParser parser) {\n      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {\n        inProtectionHeader = false;\n      }\n    }\n\n    @Override\n    public Object build() {\n      return new ProtectionElement(\n          uuid, PsshAtomUtil.buildPsshAtom(uuid, initData), buildTrackEncryptionBoxes(initData));\n    }\n\n    private static TrackEncryptionBox[] buildTrackEncryptionBoxes(byte[] initData) {\n      return new TrackEncryptionBox[] {\n        new TrackEncryptionBox(\n            /* isEncrypted= */ true,\n            /* schemeType= */ null,\n            INITIALIZATION_VECTOR_SIZE,\n            getProtectionElementKeyId(initData),\n            /* defaultEncryptedBlocks= */ 0,\n            /* defaultClearBlocks= */ 0,\n            /* defaultInitializationVector= */ null)\n      };\n    }\n\n    private static byte[] getProtectionElementKeyId(byte[] initData) {\n      StringBuilder initDataStringBuilder = new StringBuilder();\n      for (int i = 0; i < initData.length; i += 2) {\n        initDataStringBuilder.append((char) initData[i]);\n      }\n      String initDataString = initDataStringBuilder.toString();\n      String keyIdString =\n          initDataString.substring(\n              initDataString.indexOf(\"<KID>\") + 5, initDataString.indexOf(\"</KID>\"));\n      byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT);\n      swap(keyId, 0, 3);\n      swap(keyId, 1, 2);\n      swap(keyId, 4, 5);\n      swap(keyId, 6, 7);\n      return keyId;\n    }\n\n    private static void swap(byte[] data, int firstPosition, int secondPosition) {\n      byte temp = data[firstPosition];\n      data[firstPosition] = data[secondPosition];\n      data[secondPosition] = temp;\n    }\n\n    private static String stripCurlyBraces(String uuidString) {\n      if (uuidString.charAt(0) == '{' && uuidString.charAt(uuidString.length() - 1) == '}') {\n        uuidString = uuidString.substring(1, uuidString.length() - 1);\n      }\n      return uuidString;\n    }\n  }\n\n  private static class StreamIndexParser extends ElementParser {\n\n    public static final String TAG = \"StreamIndex\";\n    private static final String TAG_STREAM_FRAGMENT = \"c\";\n\n    private static final String KEY_TYPE = \"Type\";\n    private static final String KEY_TYPE_AUDIO = \"audio\";\n    private static final String KEY_TYPE_VIDEO = \"video\";\n    private static final String KEY_TYPE_TEXT = \"text\";\n    private static final String KEY_SUB_TYPE = \"Subtype\";\n    private static final String KEY_NAME = \"Name\";\n    private static final String KEY_URL = \"Url\";\n    private static final String KEY_MAX_WIDTH = \"MaxWidth\";\n    private static final String KEY_MAX_HEIGHT = \"MaxHeight\";\n    private static final String KEY_DISPLAY_WIDTH = \"DisplayWidth\";\n    private static final String KEY_DISPLAY_HEIGHT = \"DisplayHeight\";\n    private static final String KEY_LANGUAGE = \"Language\";\n    private static final String KEY_TIME_SCALE = \"TimeScale\";\n\n    private static final String KEY_FRAGMENT_DURATION = \"d\";\n    private static final String KEY_FRAGMENT_START_TIME = \"t\";\n    private static final String KEY_FRAGMENT_REPEAT_COUNT = \"r\";\n\n    private final String baseUri;\n    private final List<Format> formats;\n\n    private int type;\n    private String subType;\n    private long timescale;\n    private String name;\n    private String url;\n    private int maxWidth;\n    private int maxHeight;\n    private int displayWidth;\n    private int displayHeight;\n    private String language;\n    private ArrayList<Long> startTimes;\n\n    private long lastChunkDuration;\n\n    public StreamIndexParser(ElementParser parent, String baseUri) {\n      super(parent, baseUri, TAG);\n      this.baseUri = baseUri;\n      formats = new LinkedList<>();\n    }\n\n    @Override\n    public boolean handleChildInline(String tag) {\n      return TAG_STREAM_FRAGMENT.equals(tag);\n    }\n\n    @Override\n    public void parseStartTag(XmlPullParser parser) throws ParserException {\n      if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {\n        parseStreamFragmentStartTag(parser);\n      } else {\n        parseStreamElementStartTag(parser);\n      }\n    }\n\n    private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {\n      int chunkIndex = startTimes.size();\n      long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, C.TIME_UNSET);\n      if (startTime == C.TIME_UNSET) {\n        if (chunkIndex == 0) {\n          // Assume the track starts at t = 0.\n          startTime = 0;\n        } else if (lastChunkDuration != C.INDEX_UNSET) {\n          // Infer the start time from the previous chunk's start time and duration.\n          startTime = startTimes.get(chunkIndex - 1) + lastChunkDuration;\n        } else {\n          // We don't have the start time, and we're unable to infer it.\n          throw new ParserException(\"Unable to infer start time\");\n        }\n      }\n      chunkIndex++;\n      startTimes.add(startTime);\n      lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, C.TIME_UNSET);\n      // Handle repeated chunks.\n      long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L);\n      if (repeatCount > 1 && lastChunkDuration == C.TIME_UNSET) {\n        throw new ParserException(\"Repeated chunk with unspecified duration\");\n      }\n      for (int i = 1; i < repeatCount; i++) {\n        chunkIndex++;\n        startTimes.add(startTime + (lastChunkDuration * i));\n      }\n    }\n\n    private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {\n      type = parseType(parser);\n      putNormalizedAttribute(KEY_TYPE, type);\n      if (type == C.TRACK_TYPE_TEXT) {\n        subType = parseRequiredString(parser, KEY_SUB_TYPE);\n      } else {\n        subType = parser.getAttributeValue(null, KEY_SUB_TYPE);\n      }\n      putNormalizedAttribute(KEY_SUB_TYPE, subType);\n      name = parser.getAttributeValue(null, KEY_NAME);\n      url = parseRequiredString(parser, KEY_URL);\n      maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE);\n      maxHeight = parseInt(parser, KEY_MAX_HEIGHT, Format.NO_VALUE);\n      displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, Format.NO_VALUE);\n      displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, Format.NO_VALUE);\n      language = parser.getAttributeValue(null, KEY_LANGUAGE);\n      putNormalizedAttribute(KEY_LANGUAGE, language);\n      timescale = parseInt(parser, KEY_TIME_SCALE, -1);\n      if (timescale == -1) {\n        timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);\n      }\n      startTimes = new ArrayList<>();\n    }\n\n    private int parseType(XmlPullParser parser) throws ParserException {\n      String value = parser.getAttributeValue(null, KEY_TYPE);\n      if (value != null) {\n        if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {\n          return C.TRACK_TYPE_AUDIO;\n        } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {\n          return C.TRACK_TYPE_VIDEO;\n        } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {\n          return C.TRACK_TYPE_TEXT;\n        } else {\n          throw new ParserException(\"Invalid key value[\" + value + \"]\");\n        }\n      }\n      throw new MissingFieldException(KEY_TYPE);\n    }\n\n    @Override\n    public void addChild(Object child) {\n      if (child instanceof Format) {\n        formats.add((Format) child);\n      }\n    }\n\n    @Override\n    public Object build() {\n      Format[] formatArray = new Format[formats.size()];\n      formats.toArray(formatArray);\n      return new StreamElement(baseUri, url, type, subType, timescale, name, maxWidth, maxHeight,\n          displayWidth, displayHeight, language, formatArray, startTimes, lastChunkDuration);\n    }\n\n  }\n\n  private static class QualityLevelParser extends ElementParser {\n\n    public static final String TAG = \"QualityLevel\";\n\n    private static final String KEY_INDEX = \"Index\";\n    private static final String KEY_BITRATE = \"Bitrate\";\n    private static final String KEY_CODEC_PRIVATE_DATA = \"CodecPrivateData\";\n    private static final String KEY_SAMPLING_RATE = \"SamplingRate\";\n    private static final String KEY_CHANNELS = \"Channels\";\n    private static final String KEY_FOUR_CC = \"FourCC\";\n    private static final String KEY_TYPE = \"Type\";\n    private static final String KEY_SUB_TYPE = \"Subtype\";\n    private static final String KEY_LANGUAGE = \"Language\";\n    private static final String KEY_NAME = \"Name\";\n    private static final String KEY_MAX_WIDTH = \"MaxWidth\";\n    private static final String KEY_MAX_HEIGHT = \"MaxHeight\";\n\n    private Format format;\n\n    public QualityLevelParser(ElementParser parent, String baseUri) {\n      super(parent, baseUri, TAG);\n    }\n\n    @Override\n    public void parseStartTag(XmlPullParser parser) throws ParserException {\n      int type = (Integer) getNormalizedAttribute(KEY_TYPE);\n      String id = parser.getAttributeValue(null, KEY_INDEX);\n      String name = (String) getNormalizedAttribute(KEY_NAME);\n      int bitrate = parseRequiredInt(parser, KEY_BITRATE);\n      String sampleMimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC));\n\n      if (type == C.TRACK_TYPE_VIDEO) {\n        int width = parseRequiredInt(parser, KEY_MAX_WIDTH);\n        int height = parseRequiredInt(parser, KEY_MAX_HEIGHT);\n        List<byte[]> codecSpecificData = buildCodecSpecificData(\n            parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));\n        format =\n            Format.createVideoContainerFormat(\n                id,\n                name,\n                MimeTypes.VIDEO_MP4,\n                sampleMimeType,\n                /* codecs= */ null,\n                /* metadata= */ null,\n                bitrate,\n                width,\n                height,\n                /* frameRate= */ Format.NO_VALUE,\n                codecSpecificData,\n                /* selectionFlags= */ 0,\n                /* roleFlags= */ 0);\n      } else if (type == C.TRACK_TYPE_AUDIO) {\n        sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType;\n        int channels = parseRequiredInt(parser, KEY_CHANNELS);\n        int samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);\n        List<byte[]> codecSpecificData = buildCodecSpecificData(\n            parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));\n        if (codecSpecificData.isEmpty() && MimeTypes.AUDIO_AAC.equals(sampleMimeType)) {\n          codecSpecificData = Collections.singletonList(\n              CodecSpecificDataUtil.buildAacLcAudioSpecificConfig(samplingRate, channels));\n        }\n        String language = (String) getNormalizedAttribute(KEY_LANGUAGE);\n        format =\n            Format.createAudioContainerFormat(\n                id,\n                name,\n                MimeTypes.AUDIO_MP4,\n                sampleMimeType,\n                /* codecs= */ null,\n                /* metadata= */ null,\n                bitrate,\n                channels,\n                samplingRate,\n                codecSpecificData,\n                /* selectionFlags= */ 0,\n                /* roleFlags= */ 0,\n                language);\n      } else if (type == C.TRACK_TYPE_TEXT) {\n        String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE);\n        @C.RoleFlags int roleFlags = 0;\n        switch (subType) {\n          case \"CAPT\":\n            roleFlags = C.ROLE_FLAG_CAPTION;\n            break;\n          case \"DESC\":\n            roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;\n            break;\n          default:\n            break;\n        }\n        String language = (String) getNormalizedAttribute(KEY_LANGUAGE);\n        format =\n            Format.createTextContainerFormat(\n                id,\n                name,\n                MimeTypes.APPLICATION_MP4,\n                sampleMimeType,\n                /* codecs= */ null,\n                bitrate,\n                /* selectionFlags= */ 0,\n                roleFlags,\n                language);\n      } else {\n        format =\n            Format.createContainerFormat(\n                id,\n                name,\n                MimeTypes.APPLICATION_MP4,\n                sampleMimeType,\n                /* codecs= */ null,\n                bitrate,\n                /* selectionFlags= */ 0,\n                /* roleFlags= */ 0,\n                /* language= */ null);\n      }\n    }\n\n    @Override\n    public Object build() {\n      return format;\n    }\n\n    private static List<byte[]> buildCodecSpecificData(String codecSpecificDataString) {\n      ArrayList<byte[]> csd = new ArrayList<>();\n      if (!TextUtils.isEmpty(codecSpecificDataString)) {\n        byte[] codecPrivateData = Util.getBytesFromHexString(codecSpecificDataString);\n        byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);\n        if (split == null) {\n          csd.add(codecPrivateData);\n        } else {\n          Collections.addAll(csd, split);\n        }\n      }\n      return csd;\n    }\n\n    private static String fourCCToMimeType(String fourCC) {\n      if (fourCC.equalsIgnoreCase(\"H264\") || fourCC.equalsIgnoreCase(\"X264\")\n          || fourCC.equalsIgnoreCase(\"AVC1\") || fourCC.equalsIgnoreCase(\"DAVC\")) {\n        return MimeTypes.VIDEO_H264;\n      } else if (fourCC.equalsIgnoreCase(\"AAC\") || fourCC.equalsIgnoreCase(\"AACL\")\n          || fourCC.equalsIgnoreCase(\"AACH\") || fourCC.equalsIgnoreCase(\"AACP\")) {\n        return MimeTypes.AUDIO_AAC;\n      } else if (fourCC.equalsIgnoreCase(\"TTML\") || fourCC.equalsIgnoreCase(\"DFXP\")) {\n        return MimeTypes.APPLICATION_TTML;\n      } else if (fourCC.equalsIgnoreCase(\"ac-3\") || fourCC.equalsIgnoreCase(\"dac3\")) {\n        return MimeTypes.AUDIO_AC3;\n      } else if (fourCC.equalsIgnoreCase(\"ec-3\") || fourCC.equalsIgnoreCase(\"dec3\")) {\n        return MimeTypes.AUDIO_E_AC3;\n      } else if (fourCC.equalsIgnoreCase(\"dtsc\")) {\n        return MimeTypes.AUDIO_DTS;\n      } else if (fourCC.equalsIgnoreCase(\"dtsh\") || fourCC.equalsIgnoreCase(\"dtsl\")) {\n        return MimeTypes.AUDIO_DTS_HD;\n      } else if (fourCC.equalsIgnoreCase(\"dtse\")) {\n        return MimeTypes.AUDIO_DTS_EXPRESS;\n      } else if (fourCC.equalsIgnoreCase(\"opus\")) {\n        return MimeTypes.AUDIO_OPUS;\n      }\n      return null;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming.manifest;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.util.Util;\n\n/** SmoothStreaming related utility methods. */\npublic final class SsUtil {\n\n  /** Returns a fixed SmoothStreaming client manifest {@link Uri}. */\n  public static Uri fixManifestUri(Uri manifestUri) {\n    String lastPathSegment = manifestUri.getLastPathSegment();\n    if (lastPathSegment != null\n        && Util.toLowerInvariant(lastPathSegment).matches(\"manifest(\\\\(.+\\\\))?\")) {\n      return manifestUri;\n    }\n    return Uri.withAppendedPath(manifestUri, \"Manifest\");\n  }\n\n  private SsUtil() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.smoothstreaming.manifest;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.source.smoothstreaming.offline;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.SegmentDownloader;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A downloader for SmoothStreaming streams.\n *\n * <p>Example usage:\n *\n * <pre>{@code\n * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);\n * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory(\"ExoPlayer\", null);\n * DownloaderConstructorHelper constructorHelper =\n *     new DownloaderConstructorHelper(cache, factory);\n * // Create a downloader for the first track of the first stream element.\n * SsDownloader ssDownloader =\n *     new SsDownloader(\n *         manifestUrl,\n *         Collections.singletonList(new StreamKey(0, 0)),\n *         constructorHelper);\n * // Perform the download.\n * ssDownloader.download(progressListener);\n * // Access downloaded data using CacheDataSource\n * CacheDataSource cacheDataSource =\n *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);\n * }</pre>\n */\npublic final class SsDownloader extends SegmentDownloader<SsManifest> {\n\n  /**\n   * @param manifestUri The {@link Uri} of the manifest to be downloaded.\n   * @param streamKeys Keys defining which streams in the manifest should be selected for download.\n   *     If empty, all streams are downloaded.\n   * @param constructorHelper A {@link DownloaderConstructorHelper} instance.\n   */\n  public SsDownloader(\n      Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {\n    super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper);\n  }\n\n  @Override\n  protected SsManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException {\n    return ParsingLoadable.load(dataSource, new SsManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST);\n  }\n\n  @Override\n  protected List<Segment> getSegments(\n      DataSource dataSource, SsManifest manifest, boolean allowIncompleteList) {\n    ArrayList<Segment> segments = new ArrayList<>();\n    for (StreamElement streamElement : manifest.streamElements) {\n      for (int i = 0; i < streamElement.formats.length; i++) {\n        for (int j = 0; j < streamElement.chunkCount; j++) {\n          segments.add(\n              new Segment(\n                  streamElement.getStartTimeUs(j),\n                  new DataSpec(streamElement.buildRequestUri(i, j))));\n        }\n      }\n    }\n    return segments;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.smoothstreaming.offline;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.source.smoothstreaming;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport android.annotation.TargetApi;\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.view.accessibility.CaptioningManager;\nimport android.view.accessibility.CaptioningManager.CaptionStyle;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * A compatibility wrapper for {@link CaptionStyle}.\n */\npublic final class CaptionStyleCompat {\n\n  /**\n   * The type of edge, which may be none. One of {@link #EDGE_TYPE_NONE}, {@link\n   * #EDGE_TYPE_OUTLINE}, {@link #EDGE_TYPE_DROP_SHADOW}, {@link #EDGE_TYPE_RAISED} or {@link\n   * #EDGE_TYPE_DEPRESSED}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    EDGE_TYPE_NONE,\n    EDGE_TYPE_OUTLINE,\n    EDGE_TYPE_DROP_SHADOW,\n    EDGE_TYPE_RAISED,\n    EDGE_TYPE_DEPRESSED\n  })\n  public @interface EdgeType {}\n  /**\n   * Edge type value specifying no character edges.\n   */\n  public static final int EDGE_TYPE_NONE = 0;\n  /**\n   * Edge type value specifying uniformly outlined character edges.\n   */\n  public static final int EDGE_TYPE_OUTLINE = 1;\n  /**\n   * Edge type value specifying drop-shadowed character edges.\n   */\n  public static final int EDGE_TYPE_DROP_SHADOW = 2;\n  /**\n   * Edge type value specifying raised bevel character edges.\n   */\n  public static final int EDGE_TYPE_RAISED = 3;\n  /**\n   * Edge type value specifying depressed bevel character edges.\n   */\n  public static final int EDGE_TYPE_DEPRESSED = 4;\n\n  /**\n   * Use color setting specified by the track and fallback to default caption style.\n   */\n  public static final int USE_TRACK_COLOR_SETTINGS = 1;\n\n  /** Default caption style. */\n  public static final CaptionStyleCompat DEFAULT =\n      new CaptionStyleCompat(\n          Color.WHITE,\n          Color.BLACK,\n          Color.TRANSPARENT,\n          EDGE_TYPE_NONE,\n          Color.WHITE,\n          /* typeface= */ null);\n\n  /**\n   * The preferred foreground color.\n   */\n  public final int foregroundColor;\n\n  /**\n   * The preferred background color.\n   */\n  public final int backgroundColor;\n\n  /**\n   * The preferred window color.\n   */\n  public final int windowColor;\n\n  /**\n   * The preferred edge type. One of:\n   * <ul>\n   * <li>{@link #EDGE_TYPE_NONE}\n   * <li>{@link #EDGE_TYPE_OUTLINE}\n   * <li>{@link #EDGE_TYPE_DROP_SHADOW}\n   * <li>{@link #EDGE_TYPE_RAISED}\n   * <li>{@link #EDGE_TYPE_DEPRESSED}\n   * </ul>\n   */\n  @EdgeType public final int edgeType;\n\n  /**\n   * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}.\n   */\n  public final int edgeColor;\n\n  /** The preferred typeface, or {@code null} if unspecified. */\n  @Nullable public final Typeface typeface;\n\n  /**\n   * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}.\n   *\n   * @param captionStyle A {@link CaptionStyle}.\n   * @return The equivalent {@link CaptionStyleCompat}.\n   */\n  @TargetApi(19)\n  public static CaptionStyleCompat createFromCaptionStyle(\n      CaptionStyle captionStyle) {\n    if (Util.SDK_INT >= 21) {\n      return createFromCaptionStyleV21(captionStyle);\n    } else {\n      // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did\n      // not exist in earlier API levels).\n      return createFromCaptionStyleV19(captionStyle);\n    }\n  }\n\n  /**\n   * @param foregroundColor See {@link #foregroundColor}.\n   * @param backgroundColor See {@link #backgroundColor}.\n   * @param windowColor See {@link #windowColor}.\n   * @param edgeType See {@link #edgeType}.\n   * @param edgeColor See {@link #edgeColor}.\n   * @param typeface See {@link #typeface}.\n   */\n  public CaptionStyleCompat(\n      int foregroundColor,\n      int backgroundColor,\n      int windowColor,\n      @EdgeType int edgeType,\n      int edgeColor,\n      @Nullable Typeface typeface) {\n    this.foregroundColor = foregroundColor;\n    this.backgroundColor = backgroundColor;\n    this.windowColor = windowColor;\n    this.edgeType = edgeType;\n    this.edgeColor = edgeColor;\n    this.typeface = typeface;\n  }\n\n  @TargetApi(19)\n  @SuppressWarnings(\"ResourceType\")\n  private static CaptionStyleCompat createFromCaptionStyleV19(\n      CaptionStyle captionStyle) {\n    return new CaptionStyleCompat(\n        captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT,\n        captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface());\n  }\n\n  @TargetApi(21)\n  @SuppressWarnings(\"ResourceType\")\n  private static CaptionStyleCompat createFromCaptionStyleV21(\n      CaptionStyle captionStyle) {\n    return new CaptionStyleCompat(\n        captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor,\n        captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor,\n        captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor,\n        captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType,\n        captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor,\n        captionStyle.getTypeface());\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/Cue.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.text.Layout.Alignment;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Contains information about a specific cue, including textual content and formatting data.\n */\npublic class Cue {\n\n  /** The empty cue. */\n  public static final Cue EMPTY = new Cue(\"\");\n\n  /** An unset position, width or size. */\n  // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero.\n  public static final float DIMEN_UNSET = -Float.MAX_VALUE;\n\n  /**\n   * The type of anchor, which may be unset. One of {@link #TYPE_UNSET}, {@link #ANCHOR_TYPE_START},\n   * {@link #ANCHOR_TYPE_MIDDLE} or {@link #ANCHOR_TYPE_END}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END})\n  public @interface AnchorType {}\n\n  /**\n   * An unset anchor or line type value.\n   */\n  public static final int TYPE_UNSET = Integer.MIN_VALUE;\n\n  /**\n   * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue\n   * box.\n   */\n  public static final int ANCHOR_TYPE_START = 0;\n\n  /**\n   * Anchors the middle of the cue box.\n   */\n  public static final int ANCHOR_TYPE_MIDDLE = 1;\n\n  /**\n   * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue\n   * box.\n   */\n  public static final int ANCHOR_TYPE_END = 2;\n\n  /**\n   * The type of line, which may be unset. One of {@link #TYPE_UNSET}, {@link #LINE_TYPE_FRACTION}\n   * or {@link #LINE_TYPE_NUMBER}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER})\n  public @interface LineType {}\n\n  /**\n   * Value for {@link #lineType} when {@link #line} is a fractional position.\n   */\n  public static final int LINE_TYPE_FRACTION = 0;\n\n  /**\n   * Value for {@link #lineType} when {@link #line} is a line number.\n   */\n  public static final int LINE_TYPE_NUMBER = 1;\n\n  /**\n   * The type of default text size for this cue, which may be unset. One of {@link #TYPE_UNSET},\n   * {@link #TEXT_SIZE_TYPE_FRACTIONAL}, {@link #TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING} or {@link\n   * #TEXT_SIZE_TYPE_ABSOLUTE}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    TYPE_UNSET,\n    TEXT_SIZE_TYPE_FRACTIONAL,\n    TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,\n    TEXT_SIZE_TYPE_ABSOLUTE\n  })\n  public @interface TextSizeType {}\n\n  /** Text size is measured as a fraction of the viewport size minus the view padding. */\n  public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0;\n\n  /** Text size is measured as a fraction of the viewport size, ignoring the view padding */\n  public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1;\n\n  /** Text size is measured in number of pixels. */\n  public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2;\n\n  /**\n   * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated\n   * with styling spans.\n   */\n  @Nullable public final CharSequence text;\n\n  /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */\n  @Nullable public final Alignment textAlignment;\n\n  /** The cue image, or null if this is a text cue. */\n  @Nullable public final Bitmap bitmap;\n\n  /**\n   * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction\n   * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of\n   * the value depends on the value of {@link #lineType}.\n   * <p>\n   * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the\n   * fractional vertical position relative to the top of the viewport.\n   */\n  public final float line;\n\n  /**\n   * The type of the {@link #line} value.\n   *\n   * <p>{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the\n   * viewport.\n   *\n   * <p>{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of\n   * each line is taken to be the size of the first line of the cue. When {@link #line} is greater\n   * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset\n   * from the start edge. When {@link #line} is negative lines count from the end of the viewport,\n   * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the\n   * height of the first line of the cue, and the start and end of the viewport are the top and\n   * bottom respectively.\n   *\n   * <p>Note that it's particularly important to consider the effect of {@link #lineAnchor} when\n   * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)}\n   * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 &&\n   * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of\n   * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 &&\n   * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line\n   * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible\n   * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a\n   * cue so that only its first line is visible at the bottom of the viewport.\n   */\n  public final @LineType int lineType;\n\n  /**\n   * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link\n   * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.\n   *\n   * <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link\n   * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of\n   * the cue box respectively.\n   */\n  public final @AnchorType int lineAnchor;\n\n  /**\n   * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in\n   * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.\n   * <p>\n   * For horizontal text, this is the horizontal position relative to the left of the viewport. Note\n   * that positioning is relative to the left of the viewport even in the case of right-to-left\n   * text.\n   */\n  public final float position;\n\n  /**\n   * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link\n   * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.\n   *\n   * <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link\n   * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of\n   * the cue box respectively.\n   */\n  public final @AnchorType int positionAnchor;\n\n  /**\n   * The size of the cue box in the writing direction specified as a fraction of the viewport size\n   * in that direction, or {@link #DIMEN_UNSET}.\n   */\n  public final float size;\n\n  /**\n   * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the\n   * bitmap should be displayed at its natural height given the bitmap dimensions and the specified\n   * {@link #size}.\n   */\n  public final float bitmapHeight;\n\n  /**\n   * Specifies whether or not the {@link #windowColor} property is set.\n   */\n  public final boolean windowColorSet;\n\n  /**\n   * The fill color of the window.\n   */\n  public final int windowColor;\n\n  /**\n   * The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no\n   * default text size.\n   */\n  public final @TextSizeType int textSizeType;\n\n  /**\n   * The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default\n   * text size.\n   */\n  public final float textSize;\n\n  /**\n   * Creates an image cue.\n   *\n   * @param bitmap See {@link #bitmap}.\n   * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed\n   *     as a fraction of the viewport width.\n   * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START},\n   *     {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.\n   * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a\n   *     fraction of the viewport height.\n   * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link\n   *     #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.\n   * @param width The width of the cue as a fraction of the viewport width.\n   * @param height The height of the cue as a fraction of the viewport height, or {@link\n   *     #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified\n   *     {@code width}.\n   */\n  public Cue(\n      Bitmap bitmap,\n      float horizontalPosition,\n      @AnchorType int horizontalPositionAnchor,\n      float verticalPosition,\n      @AnchorType int verticalPositionAnchor,\n      float width,\n      float height) {\n    this(\n        /* text= */ null,\n        /* textAlignment= */ null,\n        bitmap,\n        verticalPosition,\n        /* lineType= */ LINE_TYPE_FRACTION,\n        verticalPositionAnchor,\n        horizontalPosition,\n        horizontalPositionAnchor,\n        /* textSizeType= */ TYPE_UNSET,\n        /* textSize= */ DIMEN_UNSET,\n        width,\n        height,\n        /* windowColorSet= */ false,\n        /* windowColor= */ Color.BLACK);\n  }\n\n  /**\n   * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to\n   * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.\n   *\n   * @param text See {@link #text}.\n   */\n  public Cue(CharSequence text) {\n    this(\n        text,\n        /* textAlignment= */ null,\n        /* line= */ DIMEN_UNSET,\n        /* lineType= */ TYPE_UNSET,\n        /* lineAnchor= */ TYPE_UNSET,\n        /* position= */ DIMEN_UNSET,\n        /* positionAnchor= */ TYPE_UNSET,\n        /* size= */ DIMEN_UNSET);\n  }\n\n  /**\n   * Creates a text cue.\n   *\n   * @param text See {@link #text}.\n   * @param textAlignment See {@link #textAlignment}.\n   * @param line See {@link #line}.\n   * @param lineType See {@link #lineType}.\n   * @param lineAnchor See {@link #lineAnchor}.\n   * @param position See {@link #position}.\n   * @param positionAnchor See {@link #positionAnchor}.\n   * @param size See {@link #size}.\n   */\n  public Cue(\n      CharSequence text,\n      @Nullable Alignment textAlignment,\n      float line,\n      @LineType int lineType,\n      @AnchorType int lineAnchor,\n      float position,\n      @AnchorType int positionAnchor,\n      float size) {\n    this(\n        text,\n        textAlignment,\n        line,\n        lineType,\n        lineAnchor,\n        position,\n        positionAnchor,\n        size,\n        /* windowColorSet= */ false,\n        /* windowColor= */ Color.BLACK);\n  }\n\n  /**\n   * Creates a text cue.\n   *\n   * @param text See {@link #text}.\n   * @param textAlignment See {@link #textAlignment}.\n   * @param line See {@link #line}.\n   * @param lineType See {@link #lineType}.\n   * @param lineAnchor See {@link #lineAnchor}.\n   * @param position See {@link #position}.\n   * @param positionAnchor See {@link #positionAnchor}.\n   * @param size See {@link #size}.\n   * @param textSizeType See {@link #textSizeType}.\n   * @param textSize See {@link #textSize}.\n   */\n  public Cue(\n      CharSequence text,\n      @Nullable Alignment textAlignment,\n      float line,\n      @LineType int lineType,\n      @AnchorType int lineAnchor,\n      float position,\n      @AnchorType int positionAnchor,\n      float size,\n      @TextSizeType int textSizeType,\n      float textSize) {\n    this(\n        text,\n        textAlignment,\n        /* bitmap= */ null,\n        line,\n        lineType,\n        lineAnchor,\n        position,\n        positionAnchor,\n        textSizeType,\n        textSize,\n        size,\n        /* bitmapHeight= */ DIMEN_UNSET,\n        /* windowColorSet= */ false,\n        /* windowColor= */ Color.BLACK);\n  }\n\n  /**\n   * Creates a text cue.\n   *\n   * @param text See {@link #text}.\n   * @param textAlignment See {@link #textAlignment}.\n   * @param line See {@link #line}.\n   * @param lineType See {@link #lineType}.\n   * @param lineAnchor See {@link #lineAnchor}.\n   * @param position See {@link #position}.\n   * @param positionAnchor See {@link #positionAnchor}.\n   * @param size See {@link #size}.\n   * @param windowColorSet See {@link #windowColorSet}.\n   * @param windowColor See {@link #windowColor}.\n   */\n  public Cue(\n      CharSequence text,\n      @Nullable Alignment textAlignment,\n      float line,\n      @LineType int lineType,\n      @AnchorType int lineAnchor,\n      float position,\n      @AnchorType int positionAnchor,\n      float size,\n      boolean windowColorSet,\n      int windowColor) {\n    this(\n        text,\n        textAlignment,\n        /* bitmap= */ null,\n        line,\n        lineType,\n        lineAnchor,\n        position,\n        positionAnchor,\n        /* textSizeType= */ TYPE_UNSET,\n        /* textSize= */ DIMEN_UNSET,\n        size,\n        /* bitmapHeight= */ DIMEN_UNSET,\n        windowColorSet,\n        windowColor);\n  }\n\n  private Cue(\n      @Nullable CharSequence text,\n      @Nullable Alignment textAlignment,\n      @Nullable Bitmap bitmap,\n      float line,\n      @LineType int lineType,\n      @AnchorType int lineAnchor,\n      float position,\n      @AnchorType int positionAnchor,\n      @TextSizeType int textSizeType,\n      float textSize,\n      float size,\n      float bitmapHeight,\n      boolean windowColorSet,\n      int windowColor) {\n    this.text = text;\n    this.textAlignment = textAlignment;\n    this.bitmap = bitmap;\n    this.line = line;\n    this.lineType = lineType;\n    this.lineAnchor = lineAnchor;\n    this.position = position;\n    this.positionAnchor = positionAnchor;\n    this.size = size;\n    this.bitmapHeight = bitmapHeight;\n    this.windowColorSet = windowColorSet;\n    this.windowColor = windowColor;\n    this.textSizeType = textSizeType;\n    this.textSize = textSize;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.decoder.SimpleDecoder;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.nio.ByteBuffer;\n\n/**\n * Base class for subtitle parsers that use their own decode thread.\n */\npublic abstract class SimpleSubtitleDecoder extends\n    SimpleDecoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> implements\n    SubtitleDecoder {\n\n  private final String name;\n\n  /** @param name The name of the decoder. */\n  @SuppressWarnings(\"initialization:method.invocation.invalid\")\n  protected SimpleSubtitleDecoder(String name) {\n    super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]);\n    this.name = name;\n    setInitialInputBufferSize(1024);\n  }\n\n  @Override\n  public final String getName() {\n    return name;\n  }\n\n  @Override\n  public void setPositionUs(long timeUs) {\n    // Do nothing\n  }\n\n  @Override\n  protected final SubtitleInputBuffer createInputBuffer() {\n    return new SubtitleInputBuffer();\n  }\n\n  @Override\n  protected final SubtitleOutputBuffer createOutputBuffer() {\n    return new SimpleSubtitleOutputBuffer(this);\n  }\n\n  @Override\n  protected final SubtitleDecoderException createUnexpectedDecodeException(Throwable error) {\n    return new SubtitleDecoderException(\"Unexpected decode error\", error);\n  }\n\n  @Override\n  protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) {\n    super.releaseOutputBuffer(buffer);\n  }\n\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  @Override\n  @Nullable\n  protected final SubtitleDecoderException decode(\n      SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {\n    try {\n      ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data);\n      Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset);\n      outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs);\n      // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]).\n      outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY);\n      return null;\n    } catch (SubtitleDecoderException e) {\n      return e;\n    }\n  }\n\n  /**\n   * Decodes data into a {@link Subtitle}.\n   *\n   * @param data An array holding the data to be decoded, starting at position 0.\n   * @param size The size of the data to be decoded.\n   * @param reset Whether the decoder must be reset before decoding.\n   * @return The decoded {@link Subtitle}.\n   * @throws SubtitleDecoderException If a decoding error occurs.\n   */\n  protected abstract Subtitle decode(byte[] data, int size, boolean reset)\n      throws SubtitleDecoderException;\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\n/**\n * A {@link SubtitleOutputBuffer} for decoders that extend {@link SimpleSubtitleDecoder}.\n */\n/* package */ final class SimpleSubtitleOutputBuffer extends SubtitleOutputBuffer {\n\n  private final SimpleSubtitleDecoder owner;\n\n  /**\n   * @param owner The decoder that owns this buffer.\n   */\n  public SimpleSubtitleOutputBuffer(SimpleSubtitleDecoder owner) {\n    super();\n    this.owner = owner;\n  }\n\n  @Override\n  public final void release() {\n    owner.releaseOutputBuffer(this);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/Subtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport com.google.android.exoplayer2.C;\nimport java.util.List;\n\n/**\n * A subtitle consisting of timed {@link Cue}s.\n */\npublic interface Subtitle {\n\n  /**\n   * Returns the index of the first event that occurs after a given time (exclusive).\n   *\n   * @param timeUs The time in microseconds.\n   * @return The index of the next event, or {@link C#INDEX_UNSET} if there are no events after the\n   *     specified time.\n   */\n  int getNextEventTimeIndex(long timeUs);\n\n  /**\n   * Returns the number of event times, where events are defined as points in time at which the cues\n   * returned by {@link #getCues(long)} changes.\n   *\n   * @return The number of event times.\n   */\n  int getEventTimeCount();\n\n  /**\n   * Returns the event time at a specified index.\n   *\n   * @param index The index of the event time to obtain.\n   * @return The event time in microseconds.\n   */\n  long getEventTime(int index);\n\n  /**\n   * Retrieve the cues that should be displayed at a given time.\n   *\n   * @param timeUs The time in microseconds.\n   * @return A list of cues that should be displayed, possibly empty.\n   */\n  List<Cue> getCues(long timeUs);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport com.google.android.exoplayer2.decoder.Decoder;\n\n/**\n * Decodes {@link Subtitle}s from {@link SubtitleInputBuffer}s.\n */\npublic interface SubtitleDecoder extends\n    Decoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> {\n\n  /**\n   * Informs the decoder of the current playback position.\n   * <p>\n   * Must be called prior to each attempt to dequeue output buffers from the decoder.\n   *\n   * @param positionUs The current playback position in microseconds.\n   */\n  void setPositionUs(long positionUs);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\n/**\n * Thrown when an error occurs decoding subtitle data.\n */\npublic class SubtitleDecoderException extends Exception {\n\n  /**\n   * @param message The detail message for this exception.\n   */\n  public SubtitleDecoderException(String message) {\n    super(message);\n  }\n\n  /** @param cause The cause of this exception. */\n  public SubtitleDecoderException(Exception cause) {\n    super(cause);\n  }\n\n  /**\n   * @param message The detail message for this exception.\n   * @param cause The cause of this exception.\n   */\n  public SubtitleDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.text.cea.Cea608Decoder;\nimport com.google.android.exoplayer2.text.cea.Cea708Decoder;\nimport com.google.android.exoplayer2.text.dvb.DvbDecoder;\nimport com.google.android.exoplayer2.text.pgs.PgsDecoder;\nimport com.google.android.exoplayer2.text.ssa.SsaDecoder;\nimport com.google.android.exoplayer2.text.subrip.SubripDecoder;\nimport com.google.android.exoplayer2.text.ttml.TtmlDecoder;\nimport com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;\nimport com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;\nimport com.google.android.exoplayer2.text.webvtt.WebvttDecoder;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * A factory for {@link SubtitleDecoder} instances.\n */\npublic interface SubtitleDecoderFactory {\n\n  /**\n   * Returns whether the factory is able to instantiate a {@link SubtitleDecoder} for the given\n   * {@link Format}.\n   *\n   * @param format The {@link Format}.\n   * @return Whether the factory can instantiate a suitable {@link SubtitleDecoder}.\n   */\n  boolean supportsFormat(Format format);\n\n  /**\n   * Creates a {@link SubtitleDecoder} for the given {@link Format}.\n   *\n   * @param format The {@link Format}.\n   * @return A new {@link SubtitleDecoder}.\n   * @throws IllegalArgumentException If the {@link Format} is not supported.\n   */\n  SubtitleDecoder createDecoder(Format format);\n\n  /**\n   * Default {@link SubtitleDecoderFactory} implementation.\n   *\n   * <p>The formats supported by this factory are:\n   *\n   * <ul>\n   *   <li>WebVTT ({@link WebvttDecoder})\n   *   <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})\n   *   <li>TTML ({@link TtmlDecoder})\n   *   <li>SubRip ({@link SubripDecoder})\n   *   <li>SSA/ASS ({@link SsaDecoder})\n   *   <li>TX3G ({@link Tx3gDecoder})\n   *   <li>Cea608 ({@link Cea608Decoder})\n   *   <li>Cea708 ({@link Cea708Decoder})\n   *   <li>DVB ({@link DvbDecoder})\n   *   <li>PGS ({@link PgsDecoder})\n   * </ul>\n   */\n  SubtitleDecoderFactory DEFAULT =\n      new SubtitleDecoderFactory() {\n\n        @Override\n        public boolean supportsFormat(Format format) {\n          @Nullable String mimeType = format.sampleMimeType;\n          return MimeTypes.TEXT_VTT.equals(mimeType)\n              || MimeTypes.TEXT_SSA.equals(mimeType)\n              || MimeTypes.APPLICATION_TTML.equals(mimeType)\n              || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)\n              || MimeTypes.APPLICATION_SUBRIP.equals(mimeType)\n              || MimeTypes.APPLICATION_TX3G.equals(mimeType)\n              || MimeTypes.APPLICATION_CEA608.equals(mimeType)\n              || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType)\n              || MimeTypes.APPLICATION_CEA708.equals(mimeType)\n              || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)\n              || MimeTypes.APPLICATION_PGS.equals(mimeType);\n        }\n\n        @Override\n        public SubtitleDecoder createDecoder(Format format) {\n          @Nullable String mimeType = format.sampleMimeType;\n          if (mimeType != null) {\n            switch (mimeType) {\n              case MimeTypes.TEXT_VTT:\n                return new WebvttDecoder();\n              case MimeTypes.TEXT_SSA:\n                return new SsaDecoder(format.initializationData);\n              case MimeTypes.APPLICATION_MP4VTT:\n                return new Mp4WebvttDecoder();\n              case MimeTypes.APPLICATION_TTML:\n                return new TtmlDecoder();\n              case MimeTypes.APPLICATION_SUBRIP:\n                return new SubripDecoder();\n              case MimeTypes.APPLICATION_TX3G:\n                return new Tx3gDecoder(format.initializationData);\n              case MimeTypes.APPLICATION_CEA608:\n              case MimeTypes.APPLICATION_MP4CEA608:\n                return new Cea608Decoder(mimeType, format.accessibilityChannel);\n              case MimeTypes.APPLICATION_CEA708:\n                return new Cea708Decoder(format.accessibilityChannel, format.initializationData);\n              case MimeTypes.APPLICATION_DVBSUBS:\n                return new DvbDecoder(format.initializationData);\n              case MimeTypes.APPLICATION_PGS:\n                return new PgsDecoder();\n              default:\n                break;\n            }\n          }\n          throw new IllegalArgumentException(\n              \"Attempted to create decoder for unsupported MIME type: \" + mimeType);\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\n\n/** A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. */\npublic class SubtitleInputBuffer extends DecoderInputBuffer {\n\n  /**\n   * An offset that must be added to the subtitle's event times after it's been decoded, or\n   * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.\n   */\n  public long subsampleOffsetUs;\n\n  public SubtitleInputBuffer() {\n    super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.decoder.OutputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.List;\n\n/**\n * Base class for {@link SubtitleDecoder} output buffers.\n */\npublic abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle {\n\n  @Nullable private Subtitle subtitle;\n  private long subsampleOffsetUs;\n\n  /**\n   * Sets the content of the output buffer, consisting of a {@link Subtitle} and associated\n   * metadata.\n   *\n   * @param timeUs The time of the start of the subtitle in microseconds.\n   * @param subtitle The subtitle.\n   * @param subsampleOffsetUs An offset that must be added to the subtitle's event times, or\n   *     {@link Format#OFFSET_SAMPLE_RELATIVE} if {@code timeUs} should be added.\n   */\n  public void setContent(long timeUs, Subtitle subtitle, long subsampleOffsetUs) {\n    this.timeUs = timeUs;\n    this.subtitle = subtitle;\n    this.subsampleOffsetUs = subsampleOffsetUs == Format.OFFSET_SAMPLE_RELATIVE ? this.timeUs\n        : subsampleOffsetUs;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return Assertions.checkNotNull(subtitle).getEventTimeCount();\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs);\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs);\n  }\n\n  @Override\n  public abstract void release();\n\n  @Override\n  public void clear() {\n    super.clear();\n    subtitle = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/TextOutput.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport java.util.List;\n\n/**\n * Receives text output.\n */\npublic interface TextOutput {\n\n  /**\n   * Called when there is a change in the {@link Cue}s.\n   *\n   * @param cues The {@link Cue}s. May be empty.\n   */\n  void onCues(List<Cue> cues);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text;\n\nimport android.os.Handler;\nimport android.os.Handler.Callback;\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A renderer for text.\n * <p>\n * {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained\n * from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is\n * delegated to a {@link TextOutput}.\n */\npublic final class TextRenderer extends BaseRenderer implements Callback {\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    REPLACEMENT_STATE_NONE,\n    REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,\n    REPLACEMENT_STATE_WAIT_END_OF_STREAM\n  })\n  private @interface ReplacementState {}\n  /**\n   * The decoder does not need to be replaced.\n   */\n  private static final int REPLACEMENT_STATE_NONE = 0;\n  /**\n   * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing\n   * decoder. We need to do so in order to ensure that it outputs any remaining buffers before we\n   * release it.\n   */\n  private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;\n  /**\n   * The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.\n   * We're waiting for the decoder to output an end of stream signal to indicate that it has output\n   * any remaining buffers before we release it.\n   */\n  private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;\n\n  private static final int MSG_UPDATE_OUTPUT = 0;\n\n  @Nullable private final Handler outputHandler;\n  private final TextOutput output;\n  private final SubtitleDecoderFactory decoderFactory;\n  private final FormatHolder formatHolder;\n\n  private boolean inputStreamEnded;\n  private boolean outputStreamEnded;\n  @ReplacementState private int decoderReplacementState;\n  @Nullable private Format streamFormat;\n  @Nullable private SubtitleDecoder decoder;\n  @Nullable private SubtitleInputBuffer nextInputBuffer;\n  @Nullable private SubtitleOutputBuffer subtitle;\n  @Nullable private SubtitleOutputBuffer nextSubtitle;\n  private int nextSubtitleEventIndex;\n\n  /**\n   * @param output The output.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   *     If the output makes use of standard Android UI components, then this should normally be the\n   *     looper associated with the application's main thread, which can be obtained using {@link\n   *     android.app.Activity#getMainLooper()}. Null may be passed if the output should be called\n   *     directly on the player's internal rendering thread.\n   */\n  public TextRenderer(TextOutput output, @Nullable Looper outputLooper) {\n    this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);\n  }\n\n  /**\n   * @param output The output.\n   * @param outputLooper The looper associated with the thread on which the output should be called.\n   *     If the output makes use of standard Android UI components, then this should normally be the\n   *     looper associated with the application's main thread, which can be obtained using {@link\n   *     android.app.Activity#getMainLooper()}. Null may be passed if the output should be called\n   *     directly on the player's internal rendering thread.\n   * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.\n   */\n  public TextRenderer(\n      TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {\n    super(C.TRACK_TYPE_TEXT);\n    this.output = Assertions.checkNotNull(output);\n    this.outputHandler =\n        outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);\n    this.decoderFactory = decoderFactory;\n    formatHolder = new FormatHolder();\n  }\n\n  @Override\n  @Capabilities\n  public int supportsFormat(Format format) {\n    if (decoderFactory.supportsFormat(format)) {\n      return RendererCapabilities.create(\n          supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM);\n    } else if (MimeTypes.isText(format.sampleMimeType)) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);\n    } else {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) {\n    streamFormat = formats[0];\n    if (decoder != null) {\n      decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;\n    } else {\n      decoder = decoderFactory.createDecoder(streamFormat);\n    }\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) {\n    clearOutput();\n    inputStreamEnded = false;\n    outputStreamEnded = false;\n    if (decoderReplacementState != REPLACEMENT_STATE_NONE) {\n      replaceDecoder();\n    } else {\n      releaseBuffers();\n      decoder.flush();\n    }\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (outputStreamEnded) {\n      return;\n    }\n\n    if (nextSubtitle == null) {\n      decoder.setPositionUs(positionUs);\n      try {\n        nextSubtitle = decoder.dequeueOutputBuffer();\n      } catch (SubtitleDecoderException e) {\n        throw createRendererException(e, streamFormat);\n      }\n    }\n\n    if (getState() != STATE_STARTED) {\n      return;\n    }\n\n    boolean textRendererNeedsUpdate = false;\n    if (subtitle != null) {\n      // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we\n      // advance to the next event.\n      long subtitleNextEventTimeUs = getNextEventTime();\n      while (subtitleNextEventTimeUs <= positionUs) {\n        nextSubtitleEventIndex++;\n        subtitleNextEventTimeUs = getNextEventTime();\n        textRendererNeedsUpdate = true;\n      }\n    }\n\n    if (nextSubtitle != null) {\n      if (nextSubtitle.isEndOfStream()) {\n        if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {\n          if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {\n            replaceDecoder();\n          } else {\n            releaseBuffers();\n            outputStreamEnded = true;\n          }\n        }\n      } else if (nextSubtitle.timeUs <= positionUs) {\n        // Advance to the next subtitle. Sync the next event index and trigger an update.\n        if (subtitle != null) {\n          subtitle.release();\n        }\n        subtitle = nextSubtitle;\n        nextSubtitle = null;\n        nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);\n        textRendererNeedsUpdate = true;\n      }\n    }\n\n    if (textRendererNeedsUpdate) {\n      // textRendererNeedsUpdate is set and we're playing. Update the renderer.\n      updateOutput(subtitle.getCues(positionUs));\n    }\n\n    if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {\n      return;\n    }\n\n    try {\n      while (!inputStreamEnded) {\n        if (nextInputBuffer == null) {\n          nextInputBuffer = decoder.dequeueInputBuffer();\n          if (nextInputBuffer == null) {\n            return;\n          }\n        }\n        if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {\n          nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n          decoder.queueInputBuffer(nextInputBuffer);\n          nextInputBuffer = null;\n          decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;\n          return;\n        }\n        // Try and read the next subtitle from the source.\n        int result = readSource(formatHolder, nextInputBuffer, false);\n        if (result == C.RESULT_BUFFER_READ) {\n          if (nextInputBuffer.isEndOfStream()) {\n            inputStreamEnded = true;\n          } else {\n            nextInputBuffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;\n            nextInputBuffer.flip();\n          }\n          decoder.queueInputBuffer(nextInputBuffer);\n          nextInputBuffer = null;\n        } else if (result == C.RESULT_NOTHING_READ) {\n          return;\n        }\n      }\n    } catch (SubtitleDecoderException e) {\n      throw createRendererException(e, streamFormat);\n    }\n  }\n\n  @Override\n  protected void onDisabled() {\n    streamFormat = null;\n    clearOutput();\n    releaseDecoder();\n  }\n\n  @Override\n  public boolean isEnded() {\n    return outputStreamEnded;\n  }\n\n  @Override\n  public boolean isReady() {\n    // Don't block playback whilst subtitles are loading.\n    // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].\n    return true;\n  }\n\n  private void releaseBuffers() {\n    nextInputBuffer = null;\n    nextSubtitleEventIndex = C.INDEX_UNSET;\n    if (subtitle != null) {\n      subtitle.release();\n      subtitle = null;\n    }\n    if (nextSubtitle != null) {\n      nextSubtitle.release();\n      nextSubtitle = null;\n    }\n  }\n\n  private void releaseDecoder() {\n    releaseBuffers();\n    decoder.release();\n    decoder = null;\n    decoderReplacementState = REPLACEMENT_STATE_NONE;\n  }\n\n  private void replaceDecoder() {\n    releaseDecoder();\n    decoder = decoderFactory.createDecoder(streamFormat);\n  }\n\n  private long getNextEventTime() {\n    return nextSubtitleEventIndex == C.INDEX_UNSET\n        || nextSubtitleEventIndex >= subtitle.getEventTimeCount()\n        ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);\n  }\n\n  private void updateOutput(List<Cue> cues) {\n    if (outputHandler != null) {\n      outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();\n    } else {\n      invokeUpdateOutputInternal(cues);\n    }\n  }\n\n  private void clearOutput() {\n    updateOutput(Collections.emptyList());\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public boolean handleMessage(Message msg) {\n    switch (msg.what) {\n      case MSG_UPDATE_OUTPUT:\n        invokeUpdateOutputInternal((List<Cue>) msg.obj);\n        return true;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  private void invokeUpdateOutputInternal(List<Cue> cues) {\n    output.onCues(cues);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.text.Layout.Alignment;\nimport android.text.SpannableString;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.UnderlineSpan;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleInputBuffer;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A {@link SubtitleDecoder} for CEA-608 (also known as \"line 21 captions\" and \"EIA-608\").\n */\npublic final class Cea608Decoder extends CeaDecoder {\n\n  private static final String TAG = \"Cea608Decoder\";\n\n  private static final int CC_VALID_FLAG = 0x04;\n  private static final int CC_TYPE_FLAG = 0x02;\n  private static final int CC_FIELD_FLAG = 0x01;\n\n  private static final int NTSC_CC_FIELD_1 = 0x00;\n  private static final int NTSC_CC_FIELD_2 = 0x01;\n  private static final int NTSC_CC_CHANNEL_1 = 0x00;\n  private static final int NTSC_CC_CHANNEL_2 = 0x01;\n\n  private static final int CC_MODE_UNKNOWN = 0;\n  private static final int CC_MODE_ROLL_UP = 1;\n  private static final int CC_MODE_POP_ON = 2;\n  private static final int CC_MODE_PAINT_ON = 3;\n\n  private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};\n  private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};\n\n  private static final int[] STYLE_COLORS =\n      new int[] {\n        Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA\n      };\n  private static final int STYLE_ITALICS = 0x07;\n  private static final int STYLE_UNCHANGED = 0x08;\n\n  // The default number of rows to display in roll-up captions mode.\n  private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;\n\n  // An implied first byte for packets that are only 2 bytes long, consisting of marker bits\n  // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00).\n  private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC;\n\n  /**\n   * Command initiating pop-on style captioning. Subsequent data should be loaded into a\n   * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received,\n   * at which point the non-displayed memory becomes the displayed memory (and vice versa).\n   */\n  private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;\n\n  private static final byte CTRL_BACKSPACE = 0x21;\n\n  private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;\n\n  /**\n   * Command initiating roll-up style captioning, with the maximum of 2 rows displayed\n   * simultaneously.\n   */\n  private static final byte CTRL_ROLL_UP_CAPTIONS_2_ROWS = 0x25;\n  /**\n   * Command initiating roll-up style captioning, with the maximum of 3 rows displayed\n   * simultaneously.\n   */\n  private static final byte CTRL_ROLL_UP_CAPTIONS_3_ROWS = 0x26;\n  /**\n   * Command initiating roll-up style captioning, with the maximum of 4 rows displayed\n   * simultaneously.\n   */\n  private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;\n\n  /**\n   * Command initiating paint-on style captioning. Subsequent data should be addressed immediately\n   * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.\n   */\n  private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;\n  /**\n   * TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out\n   * until a command is received that switches back to the CAPTION service.\n   */\n  private static final byte CTRL_TEXT_RESTART = 0x2A;\n\n  private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B;\n\n  private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;\n  private static final byte CTRL_CARRIAGE_RETURN = 0x2D;\n  private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;\n\n  /**\n   * Command indicating the end of a pop-on style caption. At this point the caption loaded in\n   * non-displayed memory should be swapped with the one in displayed memory. If no {@link\n   * #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into\n   * pop-on style.\n   */\n  private static final byte CTRL_END_OF_CAPTION = 0x2F;\n\n  // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).\n  private static final int[] BASIC_CHARACTER_SET = new int[] {\n    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,     //   ! \" # $ % & '\n    0x28, 0x29,                                         // ( )\n    0xE1,       // 2A: 225 'á' \"Latin small letter A with acute\"\n    0x2B, 0x2C, 0x2D, 0x2E, 0x2F,                       //       + , - . /\n    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,     // 0 1 2 3 4 5 6 7\n    0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,     // 8 9 : ; < = > ?\n    0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,     // @ A B C D E F G\n    0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,     // H I J K L M N O\n    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,     // P Q R S T U V W\n    0x58, 0x59, 0x5A, 0x5B,                             // X Y Z [\n    0xE9,       // 5C: 233 'é' \"Latin small letter E with acute\"\n    0x5D,                                               //           ]\n    0xED,       // 5E: 237 'í' \"Latin small letter I with acute\"\n    0xF3,       // 5F: 243 'ó' \"Latin small letter O with acute\"\n    0xFA,       // 60: 250 'ú' \"Latin small letter U with acute\"\n    0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,           //   a b c d e f g\n    0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,     // h i j k l m n o\n    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,     // p q r s t u v w\n    0x78, 0x79, 0x7A,                                   // x y z\n    0xE7,       // 7B: 231 'ç' \"Latin small letter C with cedilla\"\n    0xF7,       // 7C: 247 '÷' \"Division sign\"\n    0xD1,       // 7D: 209 'Ñ' \"Latin capital letter N with tilde\"\n    0xF1,       // 7E: 241 'ñ' \"Latin small letter N with tilde\"\n    0x25A0      // 7F:         \"Black Square\" (NB: 2588 = Full Block)\n  };\n\n  // Special North American 608 CC char set.\n  private static final int[] SPECIAL_CHARACTER_SET = new int[] {\n    0xAE,    // 30: 174 '®' \"Registered Sign\" - registered trademark symbol\n    0xB0,    // 31: 176 '°' \"Degree Sign\"\n    0xBD,    // 32: 189 '½' \"Vulgar Fraction One Half\" (1/2 symbol)\n    0xBF,    // 33: 191 '¿' \"Inverted Question Mark\"\n    0x2122,  // 34:         \"Trade Mark Sign\" (tm superscript)\n    0xA2,    // 35: 162 '¢' \"Cent Sign\"\n    0xA3,    // 36: 163 '£' \"Pound Sign\" - pounds sterling\n    0x266A,  // 37:         \"Eighth Note\" - music note\n    0xE0,    // 38: 224 'à' \"Latin small letter A with grave\"\n    0x20,    // 39:         TRANSPARENT SPACE - for now use ordinary space\n    0xE8,    // 3A: 232 'è' \"Latin small letter E with grave\"\n    0xE2,    // 3B: 226 'â' \"Latin small letter A with circumflex\"\n    0xEA,    // 3C: 234 'ê' \"Latin small letter E with circumflex\"\n    0xEE,    // 3D: 238 'î' \"Latin small letter I with circumflex\"\n    0xF4,    // 3E: 244 'ô' \"Latin small letter O with circumflex\"\n    0xFB     // 3F: 251 'û' \"Latin small letter U with circumflex\"\n  };\n\n  // Extended Spanish/Miscellaneous and French char set.\n  private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {\n    // Spanish and misc.\n    0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,\n    0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,\n    // French.\n    0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,\n    0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB\n  };\n\n  //Extended Portuguese and German/Danish char set.\n  private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {\n    // Portuguese.\n    0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,\n    0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,\n    // German/Danish.\n    0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,\n    0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518\n  };\n\n  private static final boolean[] ODD_PARITY_BYTE_TABLE = {\n    false, true, true, false, true, false, false, true, // 0\n    true, false, false, true, false, true, true, false, // 8\n    true, false, false, true, false, true, true, false, // 16\n    false, true, true, false, true, false, false, true, // 24\n    true, false, false, true, false, true, true, false, // 32\n    false, true, true, false, true, false, false, true, // 40\n    false, true, true, false, true, false, false, true, // 48\n    true, false, false, true, false, true, true, false, // 56\n    true, false, false, true, false, true, true, false, // 64\n    false, true, true, false, true, false, false, true, // 72\n    false, true, true, false, true, false, false, true, // 80\n    true, false, false, true, false, true, true, false, // 88\n    false, true, true, false, true, false, false, true, // 96\n    true, false, false, true, false, true, true, false, // 104\n    true, false, false, true, false, true, true, false, // 112\n    false, true, true, false, true, false, false, true, // 120\n    true, false, false, true, false, true, true, false, // 128\n    false, true, true, false, true, false, false, true, // 136\n    false, true, true, false, true, false, false, true, // 144\n    true, false, false, true, false, true, true, false, // 152\n    false, true, true, false, true, false, false, true, // 160\n    true, false, false, true, false, true, true, false, // 168\n    true, false, false, true, false, true, true, false, // 176\n    false, true, true, false, true, false, false, true, // 184\n    false, true, true, false, true, false, false, true, // 192\n    true, false, false, true, false, true, true, false, // 200\n    true, false, false, true, false, true, true, false, // 208\n    false, true, true, false, true, false, false, true, // 216\n    true, false, false, true, false, true, true, false, // 224\n    false, true, true, false, true, false, false, true, // 232\n    false, true, true, false, true, false, false, true, // 240\n    true, false, false, true, false, true, true, false, // 248\n  };\n\n  private final ParsableByteArray ccData;\n  private final int packetLength;\n  private final int selectedField;\n  private final int selectedChannel;\n  private final ArrayList<CueBuilder> cueBuilders;\n\n  private CueBuilder currentCueBuilder;\n  private List<Cue> cues;\n  private List<Cue> lastCues;\n\n  private int captionMode;\n  private int captionRowCount;\n\n  private boolean isCaptionValid;\n  private boolean repeatableControlSet;\n  private byte repeatableControlCc1;\n  private byte repeatableControlCc2;\n  private int currentChannel;\n\n  // The incoming characters may belong to 3 different services based on the last received control\n  // codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning\n  // service bytes and drops the rest.\n  private boolean isInCaptionService;\n\n  public Cea608Decoder(String mimeType, int accessibilityChannel) {\n    ccData = new ParsableByteArray();\n    cueBuilders = new ArrayList<>();\n    currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);\n    currentChannel = NTSC_CC_CHANNEL_1;\n    packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;\n    switch (accessibilityChannel) {\n      case 1:\n        selectedChannel = NTSC_CC_CHANNEL_1;\n        selectedField = NTSC_CC_FIELD_1;\n        break;\n      case 2:\n        selectedChannel = NTSC_CC_CHANNEL_2;\n        selectedField = NTSC_CC_FIELD_1;\n        break;\n      case 3:\n        selectedChannel = NTSC_CC_CHANNEL_1;\n        selectedField = NTSC_CC_FIELD_2;\n        break;\n      case 4:\n        selectedChannel = NTSC_CC_CHANNEL_2;\n        selectedField = NTSC_CC_FIELD_2;\n        break;\n      default:\n        Log.w(TAG, \"Invalid channel. Defaulting to CC1.\");\n        selectedChannel = NTSC_CC_CHANNEL_1;\n        selectedField = NTSC_CC_FIELD_1;\n    }\n\n    setCaptionMode(CC_MODE_UNKNOWN);\n    resetCueBuilders();\n    isInCaptionService = true;\n  }\n\n  @Override\n  public String getName() {\n    return \"Cea608Decoder\";\n  }\n\n  @Override\n  public void flush() {\n    super.flush();\n    cues = null;\n    lastCues = null;\n    setCaptionMode(CC_MODE_UNKNOWN);\n    setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);\n    resetCueBuilders();\n    isCaptionValid = false;\n    repeatableControlSet = false;\n    repeatableControlCc1 = 0;\n    repeatableControlCc2 = 0;\n    currentChannel = NTSC_CC_CHANNEL_1;\n    isInCaptionService = true;\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  @Override\n  protected boolean isNewSubtitleDataAvailable() {\n    return cues != lastCues;\n  }\n\n  @Override\n  protected Subtitle createSubtitle() {\n    lastCues = cues;\n    return new CeaSubtitle(cues);\n  }\n\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  @Override\n  protected void decode(SubtitleInputBuffer inputBuffer) {\n    ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());\n    boolean captionDataProcessed = false;\n    while (ccData.bytesLeft() >= packetLength) {\n      byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER\n          : (byte) ccData.readUnsignedByte();\n      int ccByte1 = ccData.readUnsignedByte();\n      int ccByte2 = ccData.readUnsignedByte();\n\n      // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according\n      // to the CEA-608 specification. We need to determine if the data should be handled\n      // differently when that is not the case.\n\n      if ((ccHeader & CC_TYPE_FLAG) != 0) {\n        // Do not process anything that is not part of the 608 byte stream.\n        continue;\n      }\n\n      if ((ccHeader & CC_FIELD_FLAG) != selectedField) {\n        // Do not process packets not within the selected field.\n        continue;\n      }\n\n      // Strip the parity bit from each byte to get CC data.\n      byte ccData1 = (byte) (ccByte1 & 0x7F);\n      byte ccData2 = (byte) (ccByte2 & 0x7F);\n\n      if (ccData1 == 0 && ccData2 == 0) {\n        // Ignore empty captions.\n        continue;\n      }\n\n      boolean previousIsCaptionValid = isCaptionValid;\n      isCaptionValid =\n          (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG\n              && ODD_PARITY_BYTE_TABLE[ccByte1]\n              && ODD_PARITY_BYTE_TABLE[ccByte2];\n\n      if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) {\n        // Ignore repeated valid commands.\n        continue;\n      }\n\n      if (!isCaptionValid) {\n        if (previousIsCaptionValid) {\n          // The encoder has flipped the validity bit to indicate captions are being turned off.\n          resetCueBuilders();\n          captionDataProcessed = true;\n        }\n        continue;\n      }\n\n      maybeUpdateIsInCaptionService(ccData1, ccData2);\n      if (!isInCaptionService) {\n        // Only the Captioning service is supported. Drop all other bytes.\n        continue;\n      }\n\n      if (!updateAndVerifyCurrentChannel(ccData1)) {\n        // Wrong channel.\n        continue;\n      }\n\n      if (isCtrlCode(ccData1)) {\n        if (isSpecialNorthAmericanChar(ccData1, ccData2)) {\n          currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2));\n        } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) {\n          // Remove standard equivalent of the special extended char before appending new one.\n          currentCueBuilder.backspace();\n          currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2));\n        } else if (isMidrowCtrlCode(ccData1, ccData2)) {\n          handleMidrowCtrl(ccData2);\n        } else if (isPreambleAddressCode(ccData1, ccData2)) {\n          handlePreambleAddressCode(ccData1, ccData2);\n        } else if (isTabCtrlCode(ccData1, ccData2)) {\n          currentCueBuilder.tabOffset = ccData2 - 0x20;\n        } else if (isMiscCode(ccData1, ccData2)) {\n          handleMiscCode(ccData2);\n        }\n      } else {\n        // Basic North American character set.\n        currentCueBuilder.append(getBasicChar(ccData1));\n        if ((ccData2 & 0xE0) != 0x00) {\n          currentCueBuilder.append(getBasicChar(ccData2));\n        }\n      }\n      captionDataProcessed = true;\n    }\n\n    if (captionDataProcessed) {\n      if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {\n        cues = getDisplayCues();\n      }\n    }\n  }\n\n  private boolean updateAndVerifyCurrentChannel(byte cc1) {\n    if (isCtrlCode(cc1)) {\n      currentChannel = getChannel(cc1);\n    }\n    return currentChannel == selectedChannel;\n  }\n\n  private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) {\n    // Most control commands are sent twice in succession to ensure they are received properly. We\n    // don't want to process duplicate commands, so if we see the same repeatable command twice in a\n    // row then we ignore the second one.\n    if (captionValid && isRepeatable(cc1)) {\n      if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {\n        // This is a repeated command, so we ignore it.\n        repeatableControlSet = false;\n        return true;\n      } else {\n        // This is the first occurrence of a repeatable command. Set the repeatable control\n        // variables so that we can recognize and ignore a duplicate (if there is one), and then\n        // continue to process the command below.\n        repeatableControlSet = true;\n        repeatableControlCc1 = cc1;\n        repeatableControlCc2 = cc2;\n      }\n    } else {\n      // This command is not repeatable.\n      repeatableControlSet = false;\n    }\n    return false;\n  }\n\n  private void handleMidrowCtrl(byte cc2) {\n    // TODO: support the extended styles (i.e. backgrounds and transparencies)\n\n    // A midrow control code advances the cursor.\n    currentCueBuilder.append(' ');\n\n    // cc2 - 0|0|1|0|STYLE|U\n    boolean underline = (cc2 & 0x01) == 0x01;\n    int style = (cc2 >> 1) & 0x07;\n    currentCueBuilder.setStyle(style, underline);\n  }\n\n  private void handlePreambleAddressCode(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|E|ROW\n    // C is the channel toggle, E is the extended flag, and ROW is the encoded row\n    int row = ROW_INDICES[cc1 & 0x07];\n    // TODO: support the extended address and style\n\n    // cc2 - 0|1|N|ATTRBTE|U\n    // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the\n    // underline toggle.\n    boolean nextRowDown = (cc2 & 0x20) != 0;\n    if (nextRowDown) {\n      row++;\n    }\n\n    if (row != currentCueBuilder.row) {\n      if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {\n        currentCueBuilder = new CueBuilder(captionMode, captionRowCount);\n        cueBuilders.add(currentCueBuilder);\n      }\n      currentCueBuilder.row = row;\n    }\n\n    // cc2 - 0|1|N|0|STYLE|U\n    // cc2 - 0|1|N|1|CURSR|U\n    boolean isCursor = (cc2 & 0x10) == 0x10;\n    boolean underline = (cc2 & 0x01) == 0x01;\n    int cursorOrStyle = (cc2 >> 1) & 0x07;\n\n    // We need to call setStyle even for the isCursor case, to update the underline bit.\n    // STYLE_UNCHANGED is used for this case.\n    currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);\n\n    if (isCursor) {\n      currentCueBuilder.indent = COLUMN_INDICES[cursorOrStyle];\n    }\n  }\n\n  private void handleMiscCode(byte cc2) {\n    switch (cc2) {\n      case CTRL_ROLL_UP_CAPTIONS_2_ROWS:\n        setCaptionMode(CC_MODE_ROLL_UP);\n        setCaptionRowCount(2);\n        return;\n      case CTRL_ROLL_UP_CAPTIONS_3_ROWS:\n        setCaptionMode(CC_MODE_ROLL_UP);\n        setCaptionRowCount(3);\n        return;\n      case CTRL_ROLL_UP_CAPTIONS_4_ROWS:\n        setCaptionMode(CC_MODE_ROLL_UP);\n        setCaptionRowCount(4);\n        return;\n      case CTRL_RESUME_CAPTION_LOADING:\n        setCaptionMode(CC_MODE_POP_ON);\n        return;\n      case CTRL_RESUME_DIRECT_CAPTIONING:\n        setCaptionMode(CC_MODE_PAINT_ON);\n        return;\n      default:\n        // Fall through.\n        break;\n    }\n\n    if (captionMode == CC_MODE_UNKNOWN) {\n      return;\n    }\n\n    switch (cc2) {\n      case CTRL_ERASE_DISPLAYED_MEMORY:\n        cues = Collections.emptyList();\n        if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {\n          resetCueBuilders();\n        }\n        break;\n      case CTRL_ERASE_NON_DISPLAYED_MEMORY:\n        resetCueBuilders();\n        break;\n      case CTRL_END_OF_CAPTION:\n        cues = getDisplayCues();\n        resetCueBuilders();\n        break;\n      case CTRL_CARRIAGE_RETURN:\n        // carriage returns only apply to rollup captions; don't bother if we don't have anything\n        // to add a carriage return to\n        if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {\n          currentCueBuilder.rollUp();\n        }\n        break;\n      case CTRL_BACKSPACE:\n        currentCueBuilder.backspace();\n        break;\n      case CTRL_DELETE_TO_END_OF_ROW:\n        // TODO: implement\n        break;\n      default:\n        // Fall through.\n        break;\n    }\n  }\n\n  private List<Cue> getDisplayCues() {\n    // CEA-608 does not define middle and end alignment, however content providers artificially\n    // introduce them using whitespace. When each cue is built, we try and infer the alignment based\n    // on the amount of whitespace either side of the text. To avoid consecutive cues being aligned\n    // differently, we force all cues to have the same alignment, with start alignment given\n    // preference, then middle alignment, then end alignment.\n    @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_END;\n    int cueBuilderCount = cueBuilders.size();\n    List<Cue> cueBuilderCues = new ArrayList<>(cueBuilderCount);\n    for (int i = 0; i < cueBuilderCount; i++) {\n      Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET);\n      cueBuilderCues.add(cue);\n      if (cue != null) {\n        positionAnchor = Math.min(positionAnchor, cue.positionAnchor);\n      }\n    }\n\n    // Skip null cues and rebuild any that don't have the preferred alignment.\n    List<Cue> displayCues = new ArrayList<>(cueBuilderCount);\n    for (int i = 0; i < cueBuilderCount; i++) {\n      Cue cue = cueBuilderCues.get(i);\n      if (cue != null) {\n        if (cue.positionAnchor != positionAnchor) {\n          cue = cueBuilders.get(i).build(positionAnchor);\n        }\n        displayCues.add(cue);\n      }\n    }\n\n    return displayCues;\n  }\n\n  private void setCaptionMode(int captionMode) {\n    if (this.captionMode == captionMode) {\n      return;\n    }\n\n    int oldCaptionMode = this.captionMode;\n    this.captionMode = captionMode;\n\n    if (captionMode == CC_MODE_PAINT_ON) {\n      // Switching to paint-on mode should have no effect except to select the mode.\n      for (int i = 0; i < cueBuilders.size(); i++) {\n        cueBuilders.get(i).setCaptionMode(captionMode);\n      }\n      return;\n    }\n\n    // Clear the working memory.\n    resetCueBuilders();\n    if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP\n        || captionMode == CC_MODE_UNKNOWN) {\n      // When switching from paint-on or to roll-up or unknown, we also need to clear the caption.\n      cues = Collections.emptyList();\n    }\n  }\n\n  private void setCaptionRowCount(int captionRowCount) {\n    this.captionRowCount = captionRowCount;\n    currentCueBuilder.setCaptionRowCount(captionRowCount);\n  }\n\n  private void resetCueBuilders() {\n    currentCueBuilder.reset(captionMode);\n    cueBuilders.clear();\n    cueBuilders.add(currentCueBuilder);\n  }\n\n  private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) {\n    if (isXdsControlCode(cc1)) {\n      isInCaptionService = false;\n    } else if (isServiceSwitchCommand(cc1)) {\n      switch (cc2) {\n        case CTRL_TEXT_RESTART:\n        case CTRL_RESUME_TEXT_DISPLAY:\n          isInCaptionService = false;\n          break;\n        case CTRL_END_OF_CAPTION:\n        case CTRL_RESUME_CAPTION_LOADING:\n        case CTRL_RESUME_DIRECT_CAPTIONING:\n        case CTRL_ROLL_UP_CAPTIONS_2_ROWS:\n        case CTRL_ROLL_UP_CAPTIONS_3_ROWS:\n        case CTRL_ROLL_UP_CAPTIONS_4_ROWS:\n          isInCaptionService = true;\n          break;\n        default:\n          // No update.\n      }\n    }\n  }\n\n  private static char getBasicChar(byte ccData) {\n    int index = (ccData & 0x7F) - 0x20;\n    return (char) BASIC_CHARACTER_SET[index];\n  }\n\n  private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|0|0|1\n    // cc2 - 0|0|1|1|X|X|X|X\n    return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30);\n  }\n\n  private static char getSpecialNorthAmericanChar(byte ccData) {\n    int index = ccData & 0x0F;\n    return (char) SPECIAL_CHARACTER_SET[index];\n  }\n\n  private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|0|1|S\n    // cc2 - 0|0|1|X|X|X|X|X\n    return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20);\n  }\n\n  private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) {\n    if ((cc1 & 0x01) == 0x00) {\n      // Extended Spanish/Miscellaneous and French character set (S = 0).\n      return getExtendedEsFrChar(cc2);\n    } else {\n      // Extended Portuguese and German/Danish character set (S = 1).\n      return getExtendedPtDeChar(cc2);\n    }\n  }\n\n  private static char getExtendedEsFrChar(byte ccData) {\n    int index = ccData & 0x1F;\n    return (char) SPECIAL_ES_FR_CHARACTER_SET[index];\n  }\n\n  private static char getExtendedPtDeChar(byte ccData) {\n    int index = ccData & 0x1F;\n    return (char) SPECIAL_PT_DE_CHARACTER_SET[index];\n  }\n\n  private static boolean isCtrlCode(byte cc1) {\n    // cc1 - 0|0|0|X|X|X|X|X\n    return (cc1 & 0xE0) == 0x00;\n  }\n\n  private static int getChannel(byte cc1) {\n    // cc1 - X|X|X|X|C|X|X|X\n    return (cc1 >> 3) & 0x1;\n  }\n\n  private static boolean isMidrowCtrlCode(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|0|0|1\n    // cc2 - 0|0|1|0|X|X|X|X\n    return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);\n  }\n\n  private static boolean isPreambleAddressCode(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|X|X|X\n    // cc2 - 0|1|X|X|X|X|X|X\n    return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40);\n  }\n\n  private static boolean isTabCtrlCode(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|1|1|1\n    // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1\n    return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);\n  }\n\n  private static boolean isMiscCode(byte cc1, byte cc2) {\n    // cc1 - 0|0|0|1|C|1|0|F\n    // cc2 - 0|0|1|0|X|X|X|X\n    return ((cc1 & 0xF6) == 0x14) && ((cc2 & 0xF0) == 0x20);\n  }\n\n  private static boolean isRepeatable(byte cc1) {\n    // cc1 - 0|0|0|1|X|X|X|X\n    return (cc1 & 0xF0) == 0x10;\n  }\n\n  private static boolean isXdsControlCode(byte cc1) {\n    return 0x01 <= cc1 && cc1 <= 0x0F;\n  }\n\n  private static boolean isServiceSwitchCommand(byte cc1) {\n    // cc1 - 0|0|0|1|C|1|0|0\n    return (cc1 & 0xF7) == 0x14;\n  }\n\n  private static class CueBuilder {\n\n    // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608\n    // positions to normalized screen position.\n    private static final int SCREEN_CHARWIDTH = 32;\n    private static final int BASE_ROW = 15;\n\n    private final List<CueStyle> cueStyles;\n    private final List<SpannableString> rolledUpCaptions;\n    private final StringBuilder captionStringBuilder;\n\n    private int row;\n    private int indent;\n    private int tabOffset;\n    private int captionMode;\n    private int captionRowCount;\n\n    public CueBuilder(int captionMode, int captionRowCount) {\n      cueStyles = new ArrayList<>();\n      rolledUpCaptions = new ArrayList<>();\n      captionStringBuilder = new StringBuilder();\n      reset(captionMode);\n      setCaptionRowCount(captionRowCount);\n    }\n\n    public void reset(int captionMode) {\n      this.captionMode = captionMode;\n      cueStyles.clear();\n      rolledUpCaptions.clear();\n      captionStringBuilder.setLength(0);\n      row = BASE_ROW;\n      indent = 0;\n      tabOffset = 0;\n    }\n\n    public boolean isEmpty() {\n      return cueStyles.isEmpty()\n          && rolledUpCaptions.isEmpty()\n          && captionStringBuilder.length() == 0;\n    }\n\n    public void setCaptionMode(int captionMode) {\n      this.captionMode = captionMode;\n    }\n\n    public void setCaptionRowCount(int captionRowCount) {\n      this.captionRowCount = captionRowCount;\n    }\n\n    public void setStyle(int style, boolean underline) {\n      cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));\n    }\n\n    public void backspace() {\n      int length = captionStringBuilder.length();\n      if (length > 0) {\n        captionStringBuilder.delete(length - 1, length);\n        // Decrement style start positions if necessary.\n        for (int i = cueStyles.size() - 1; i >= 0; i--) {\n          CueStyle style = cueStyles.get(i);\n          if (style.start == length) {\n            style.start--;\n          } else {\n            // All earlier cues must have style.start < length.\n            break;\n          }\n        }\n      }\n    }\n\n    public void append(char text) {\n      captionStringBuilder.append(text);\n    }\n\n    public void rollUp() {\n      rolledUpCaptions.add(buildCurrentLine());\n      captionStringBuilder.setLength(0);\n      cueStyles.clear();\n      int numRows = Math.min(captionRowCount, row);\n      while (rolledUpCaptions.size() >= numRows) {\n        rolledUpCaptions.remove(0);\n      }\n    }\n\n    public Cue build(@Cue.AnchorType int forcedPositionAnchor) {\n      SpannableStringBuilder cueString = new SpannableStringBuilder();\n      // Add any rolled up captions, separated by new lines.\n      for (int i = 0; i < rolledUpCaptions.size(); i++) {\n        cueString.append(rolledUpCaptions.get(i));\n        cueString.append('\\n');\n      }\n      // Add the current line.\n      cueString.append(buildCurrentLine());\n\n      if (cueString.length() == 0) {\n        // The cue is empty.\n        return null;\n      }\n\n      int positionAnchor;\n      // The number of empty columns before the start of the text, in the range [0-31].\n      int startPadding = indent + tabOffset;\n      // The number of empty columns after the end of the text, in the same range.\n      int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();\n      int startEndPaddingDelta = startPadding - endPadding;\n      if (forcedPositionAnchor != Cue.TYPE_UNSET) {\n        positionAnchor = forcedPositionAnchor;\n      } else if (captionMode == CC_MODE_POP_ON\n          && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {\n        // Treat approximately centered pop-on captions as middle aligned. We also treat captions\n        // that are wider than they should be in this way. See\n        // https://github.com/google/ExoPlayer/issues/3534.\n        positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;\n      } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {\n        // Treat pop-on captions with less padding at the end than the start as end aligned.\n        positionAnchor = Cue.ANCHOR_TYPE_END;\n      } else {\n        // For all other cases assume start aligned.\n        positionAnchor = Cue.ANCHOR_TYPE_START;\n      }\n\n      float position;\n      switch (positionAnchor) {\n        case Cue.ANCHOR_TYPE_MIDDLE:\n          position = 0.5f;\n          break;\n        case Cue.ANCHOR_TYPE_END:\n          position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH;\n          // Adjust the position to fit within the safe area.\n          position = position * 0.8f + 0.1f;\n          break;\n        case Cue.ANCHOR_TYPE_START:\n        default:\n          position = (float) startPadding / SCREEN_CHARWIDTH;\n          // Adjust the position to fit within the safe area.\n          position = position * 0.8f + 0.1f;\n          break;\n      }\n\n      int lineAnchor;\n      int line;\n      // Note: Row indices are in the range [1-15].\n      if (captionMode == CC_MODE_ROLL_UP || row > (BASE_ROW / 2)) {\n        lineAnchor = Cue.ANCHOR_TYPE_END;\n        line = row - BASE_ROW;\n        // Two line adjustments. The first is because line indices from the bottom of the window\n        // start from -1 rather than 0. The second is a blank row to act as the safe area.\n        line -= 2;\n      } else {\n        lineAnchor = Cue.ANCHOR_TYPE_START;\n        // Line indices from the top of the window start from 0, but we want a blank row to act as\n        // the safe area. As a result no adjustment is necessary.\n        line = row;\n      }\n\n      return new Cue(\n          cueString,\n          Alignment.ALIGN_NORMAL,\n          line,\n          Cue.LINE_TYPE_NUMBER,\n          lineAnchor,\n          position,\n          positionAnchor,\n          Cue.DIMEN_UNSET);\n    }\n\n    private SpannableString buildCurrentLine() {\n      SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);\n      int length = builder.length();\n\n      int underlineStartPosition = C.INDEX_UNSET;\n      int italicStartPosition = C.INDEX_UNSET;\n      int colorStartPosition = 0;\n      int color = Color.WHITE;\n\n      boolean nextItalic = false;\n      int nextColor = Color.WHITE;\n\n      for (int i = 0; i < cueStyles.size(); i++) {\n        CueStyle cueStyle = cueStyles.get(i);\n        boolean underline = cueStyle.underline;\n        int style = cueStyle.style;\n        if (style != STYLE_UNCHANGED) {\n          // If the style is a color then italic is cleared.\n          nextItalic = style == STYLE_ITALICS;\n          // If the style is italic then the color is left unchanged.\n          nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];\n        }\n\n        int position = cueStyle.start;\n        int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;\n        if (position == nextPosition) {\n          // There are more cueStyles to process at the current position.\n          continue;\n        }\n\n        // Process changes to underline up to the current position.\n        if (underlineStartPosition != C.INDEX_UNSET && !underline) {\n          setUnderlineSpan(builder, underlineStartPosition, position);\n          underlineStartPosition = C.INDEX_UNSET;\n        } else if (underlineStartPosition == C.INDEX_UNSET && underline) {\n          underlineStartPosition = position;\n        }\n        // Process changes to italic up to the current position.\n        if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {\n          setItalicSpan(builder, italicStartPosition, position);\n          italicStartPosition = C.INDEX_UNSET;\n        } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {\n          italicStartPosition = position;\n        }\n        // Process changes to color up to the current position.\n        if (nextColor != color) {\n          setColorSpan(builder, colorStartPosition, position, color);\n          color = nextColor;\n          colorStartPosition = position;\n        }\n      }\n\n      // Add any final spans.\n      if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {\n        setUnderlineSpan(builder, underlineStartPosition, length);\n      }\n      if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {\n        setItalicSpan(builder, italicStartPosition, length);\n      }\n      if (colorStartPosition != length) {\n        setColorSpan(builder, colorStartPosition, length, color);\n      }\n\n      return new SpannableString(builder);\n    }\n\n    private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {\n      builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n\n    private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {\n      builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n\n    private static void setColorSpan(\n        SpannableStringBuilder builder, int start, int end, int color) {\n      if (color == Color.WHITE) {\n        // White is treated as the default color (i.e. no span is attached).\n        return;\n      }\n      builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n\n    private static class CueStyle {\n\n      public final int style;\n      public final boolean underline;\n\n      public int start;\n\n      public CueStyle(int style, boolean underline, int start) {\n        this.style = style;\n        this.underline = underline;\n        this.start = start;\n      }\n\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport android.text.Layout.Alignment;\nimport androidx.annotation.NonNull;\nimport com.google.android.exoplayer2.text.Cue;\n\n/**\n * A {@link Cue} for CEA-708.\n */\n/* package */ final class Cea708Cue extends Cue implements Comparable<Cea708Cue> {\n\n  /**\n   * The priority of the cue box.\n   */\n  public final int priority;\n\n  /**\n   * @param text See {@link #text}.\n   * @param textAlignment See {@link #textAlignment}.\n   * @param line See {@link #line}.\n   * @param lineType See {@link #lineType}.\n   * @param lineAnchor See {@link #lineAnchor}.\n   * @param position See {@link #position}.\n   * @param positionAnchor See {@link #positionAnchor}.\n   * @param size See {@link #size}.\n   * @param windowColorSet See {@link #windowColorSet}.\n   * @param windowColor See {@link #windowColor}.\n   * @param priority See (@link #priority}.\n   */\n  public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,\n      @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,\n      boolean windowColorSet, int windowColor, int priority) {\n    super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size,\n        windowColorSet, windowColor);\n    this.priority = priority;\n  }\n\n  @Override\n  public int compareTo(@NonNull Cea708Cue other) {\n    if (other.priority < priority) {\n      return -1;\n    } else if (other.priority > priority) {\n      return 1;\n    }\n    return 0;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.text.Layout.Alignment;\nimport android.text.SpannableString;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.UnderlineSpan;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Cue.AnchorType;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A {@link SubtitleDecoder} for CEA-708 (also known as \"EIA-708\").\n */\npublic final class Cea708Decoder extends CeaDecoder {\n\n  private static final String TAG = \"Cea708Decoder\";\n\n  private static final int NUM_WINDOWS = 8;\n\n  private static final int DTVCC_PACKET_DATA = 0x02;\n  private static final int DTVCC_PACKET_START = 0x03;\n  private static final int CC_VALID_FLAG = 0x04;\n\n  // Base Commands\n  private static final int GROUP_C0_END = 0x1F;  // Miscellaneous Control Codes\n  private static final int GROUP_G0_END = 0x7F;  // ASCII Printable Characters\n  private static final int GROUP_C1_END = 0x9F;  // Captioning Command Control Codes\n  private static final int GROUP_G1_END = 0xFF;  // ISO 8859-1 LATIN-1 Character Set\n\n  // Extended Commands\n  private static final int GROUP_C2_END = 0x1F;  // Extended Control Code Set 1\n  private static final int GROUP_G2_END = 0x7F;  // Extended Miscellaneous Characters\n  private static final int GROUP_C3_END = 0x9F;  // Extended Control Code Set 2\n  private static final int GROUP_G3_END = 0xFF;  // Future Expansion\n\n  // Group C0 Commands\n  private static final int COMMAND_NUL = 0x00;        // Nul\n  private static final int COMMAND_ETX = 0x03;        // EndOfText\n  private static final int COMMAND_BS = 0x08;         // Backspace\n  private static final int COMMAND_FF = 0x0C;         // FormFeed (Flush)\n  private static final int COMMAND_CR = 0x0D;         // CarriageReturn\n  private static final int COMMAND_HCR = 0x0E;        // ClearLine\n  private static final int COMMAND_EXT1 = 0x10;       // Extended Control Code Flag\n  private static final int COMMAND_EXT1_START = 0x11;\n  private static final int COMMAND_EXT1_END = 0x17;\n  private static final int COMMAND_P16_START = 0x18;\n  private static final int COMMAND_P16_END = 0x1F;\n\n  // Group C1 Commands\n  private static final int COMMAND_CW0 = 0x80;  // SetCurrentWindow to 0\n  private static final int COMMAND_CW1 = 0x81;  // SetCurrentWindow to 1\n  private static final int COMMAND_CW2 = 0x82;  // SetCurrentWindow to 2\n  private static final int COMMAND_CW3 = 0x83;  // SetCurrentWindow to 3\n  private static final int COMMAND_CW4 = 0x84;  // SetCurrentWindow to 4\n  private static final int COMMAND_CW5 = 0x85;  // SetCurrentWindow to 5\n  private static final int COMMAND_CW6 = 0x86;  // SetCurrentWindow to 6\n  private static final int COMMAND_CW7 = 0x87;  // SetCurrentWindow to 7\n  private static final int COMMAND_CLW = 0x88;  // ClearWindows (+1 byte)\n  private static final int COMMAND_DSW = 0x89;  // DisplayWindows (+1 byte)\n  private static final int COMMAND_HDW = 0x8A;  // HideWindows (+1 byte)\n  private static final int COMMAND_TGW = 0x8B;  // ToggleWindows (+1 byte)\n  private static final int COMMAND_DLW = 0x8C;  // DeleteWindows (+1 byte)\n  private static final int COMMAND_DLY = 0x8D;  // Delay (+1 byte)\n  private static final int COMMAND_DLC = 0x8E;  // DelayCancel\n  private static final int COMMAND_RST = 0x8F;  // Reset\n  private static final int COMMAND_SPA = 0x90;  // SetPenAttributes (+2 bytes)\n  private static final int COMMAND_SPC = 0x91;  // SetPenColor (+3 bytes)\n  private static final int COMMAND_SPL = 0x92;  // SetPenLocation (+2 bytes)\n  private static final int COMMAND_SWA = 0x97;  // SetWindowAttributes (+4 bytes)\n  private static final int COMMAND_DF0 = 0x98;  // DefineWindow 0 (+6 bytes)\n  private static final int COMMAND_DF1 = 0x99;  // DefineWindow 1 (+6 bytes)\n  private static final int COMMAND_DF2 = 0x9A;  // DefineWindow 2 (+6 bytes)\n  private static final int COMMAND_DF3 = 0x9B;  // DefineWindow 3 (+6 bytes)\n  private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes)\n  private static final int COMMAND_DF5 = 0x9D;  // DefineWindow 5 (+6 bytes)\n  private static final int COMMAND_DF6 = 0x9E;  // DefineWindow 6 (+6 bytes)\n  private static final int COMMAND_DF7 = 0x9F;  // DefineWindow 7 (+6 bytes)\n\n  // G0 Table Special Chars\n  private static final int CHARACTER_MN = 0x7F;  // MusicNote\n\n  // G2 Table Special Chars\n  private static final int CHARACTER_TSP = 0x20;\n  private static final int CHARACTER_NBTSP = 0x21;\n  private static final int CHARACTER_ELLIPSIS = 0x25;\n  private static final int CHARACTER_BIG_CARONS = 0x2A;\n  private static final int CHARACTER_BIG_OE = 0x2C;\n  private static final int CHARACTER_SOLID_BLOCK = 0x30;\n  private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;\n  private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;\n  private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;\n  private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;\n  private static final int CHARACTER_BOLD_BULLET = 0x35;\n  private static final int CHARACTER_TM = 0x39;\n  private static final int CHARACTER_SMALL_CARONS = 0x3A;\n  private static final int CHARACTER_SMALL_OE = 0x3C;\n  private static final int CHARACTER_SM = 0x3D;\n  private static final int CHARACTER_DIAERESIS_Y = 0x3F;\n  private static final int CHARACTER_ONE_EIGHTH = 0x76;\n  private static final int CHARACTER_THREE_EIGHTHS = 0x77;\n  private static final int CHARACTER_FIVE_EIGHTHS = 0x78;\n  private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;\n  private static final int CHARACTER_VERTICAL_BORDER = 0x7A;\n  private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;\n  private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;\n  private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;\n  private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;\n  private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;\n\n  private final ParsableByteArray ccData;\n  private final ParsableBitArray serviceBlockPacket;\n\n  private final int selectedServiceNumber;\n  private final CueBuilder[] cueBuilders;\n\n  private CueBuilder currentCueBuilder;\n  private List<Cue> cues;\n  private List<Cue> lastCues;\n\n  private DtvCcPacket currentDtvCcPacket;\n  private int currentWindow;\n\n  // TODO: Retrieve isWideAspectRatio from initializationData and use it.\n  public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) {\n    ccData = new ParsableByteArray();\n    serviceBlockPacket = new ParsableBitArray();\n    selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;\n\n    cueBuilders = new CueBuilder[NUM_WINDOWS];\n    for (int i = 0; i < NUM_WINDOWS; i++) {\n      cueBuilders[i] = new CueBuilder();\n    }\n\n    currentCueBuilder = cueBuilders[0];\n    resetCueBuilders();\n  }\n\n  @Override\n  public String getName() {\n    return \"Cea708Decoder\";\n  }\n\n  @Override\n  public void flush() {\n    super.flush();\n    cues = null;\n    lastCues = null;\n    currentWindow = 0;\n    currentCueBuilder = cueBuilders[currentWindow];\n    resetCueBuilders();\n    currentDtvCcPacket = null;\n  }\n\n  @Override\n  protected boolean isNewSubtitleDataAvailable() {\n    return cues != lastCues;\n  }\n\n  @Override\n  protected Subtitle createSubtitle() {\n    lastCues = cues;\n    return new CeaSubtitle(cues);\n  }\n\n  @Override\n  protected void decode(SubtitleInputBuffer inputBuffer) {\n    // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe.\n    @SuppressWarnings(\"ByteBufferBackingArray\")\n    byte[] inputBufferData = inputBuffer.data.array();\n    ccData.reset(inputBufferData, inputBuffer.data.limit());\n    while (ccData.bytesLeft() >= 3) {\n      int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);\n\n      int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);\n      boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;\n      byte ccData1 = (byte) ccData.readUnsignedByte();\n      byte ccData2 = (byte) ccData.readUnsignedByte();\n\n      // Ignore any non-CEA-708 data\n      if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {\n        continue;\n      }\n\n      if (!ccValid) {\n        // This byte-pair isn't valid, ignore it and continue.\n        continue;\n      }\n\n      if (ccType == DTVCC_PACKET_START) {\n        finalizeCurrentPacket();\n\n        int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits\n        int packetSize = ccData1 & 0x3F; // last 6 bits\n        if (packetSize == 0) {\n          packetSize = 64;\n        }\n\n        currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);\n        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;\n      } else {\n        // The only remaining valid packet type is DTVCC_PACKET_DATA\n        Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);\n\n        if (currentDtvCcPacket == null) {\n          Log.e(TAG, \"Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START\");\n          continue;\n        }\n\n        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;\n        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;\n      }\n\n      if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {\n        finalizeCurrentPacket();\n      }\n    }\n  }\n\n  private void finalizeCurrentPacket() {\n    if (currentDtvCcPacket == null) {\n      // No packet to finalize;\n      return;\n    }\n\n    processCurrentPacket();\n    currentDtvCcPacket = null;\n  }\n\n  private void processCurrentPacket() {\n    if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {\n      Log.w(TAG, \"DtvCcPacket ended prematurely; size is \" + (currentDtvCcPacket.packetSize * 2 - 1)\n          + \", but current index is \" + currentDtvCcPacket.currentIndex + \" (sequence number \"\n          + currentDtvCcPacket.sequenceNumber + \"); ignoring packet\");\n      return;\n    }\n\n    serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);\n\n    int serviceNumber = serviceBlockPacket.readBits(3);\n    int blockSize = serviceBlockPacket.readBits(5);\n    if (serviceNumber == 7) {\n      // extended service numbers\n      serviceBlockPacket.skipBits(2);\n      serviceNumber = serviceBlockPacket.readBits(6);\n      if (serviceNumber < 7) {\n        Log.w(TAG, \"Invalid extended service number: \" + serviceNumber);\n      }\n    }\n\n    // Ignore packets in which blockSize is 0\n    if (blockSize == 0) {\n      if (serviceNumber != 0) {\n        Log.w(TAG, \"serviceNumber is non-zero (\" + serviceNumber + \") when blockSize is 0\");\n      }\n      return;\n    }\n\n    if (serviceNumber != selectedServiceNumber) {\n      return;\n    }\n\n    // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after\n    // processing the service block any text has been added to the buffer. See CEA-708-B Section\n    // 8.10.4 for more details.\n    boolean cuesNeedUpdate = false;\n\n    while (serviceBlockPacket.bitsLeft() > 0) {\n      int command = serviceBlockPacket.readBits(8);\n      if (command != COMMAND_EXT1) {\n        if (command <= GROUP_C0_END) {\n          handleC0Command(command);\n          // If the C0 command was an ETX command, the cues are updated in handleC0Command.\n        } else if (command <= GROUP_G0_END) {\n          handleG0Character(command);\n          cuesNeedUpdate = true;\n        } else if (command <= GROUP_C1_END) {\n          handleC1Command(command);\n          cuesNeedUpdate = true;\n        } else if (command <= GROUP_G1_END) {\n          handleG1Character(command);\n          cuesNeedUpdate = true;\n        } else {\n          Log.w(TAG, \"Invalid base command: \" + command);\n        }\n      } else {\n        // Read the extended command\n        command = serviceBlockPacket.readBits(8);\n        if (command <= GROUP_C2_END) {\n          handleC2Command(command);\n        } else if (command <= GROUP_G2_END) {\n          handleG2Character(command);\n          cuesNeedUpdate = true;\n        } else if (command <= GROUP_C3_END) {\n          handleC3Command(command);\n        } else if (command <= GROUP_G3_END) {\n          handleG3Character(command);\n          cuesNeedUpdate = true;\n        } else {\n          Log.w(TAG, \"Invalid extended command: \" + command);\n        }\n      }\n    }\n\n    if (cuesNeedUpdate) {\n      cues = getDisplayCues();\n    }\n  }\n\n  private void handleC0Command(int command) {\n    switch (command) {\n      case COMMAND_NUL:\n        // Do nothing.\n        break;\n      case COMMAND_ETX:\n        cues = getDisplayCues();\n        break;\n      case COMMAND_BS:\n        currentCueBuilder.backspace();\n        break;\n      case COMMAND_FF:\n        resetCueBuilders();\n        break;\n      case COMMAND_CR:\n        currentCueBuilder.append('\\n');\n        break;\n      case COMMAND_HCR:\n        // TODO: Add support for this command.\n        break;\n      default:\n        if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {\n          Log.w(TAG, \"Currently unsupported COMMAND_EXT1 Command: \" + command);\n          serviceBlockPacket.skipBits(8);\n        } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {\n          Log.w(TAG, \"Currently unsupported COMMAND_P16 Command: \" + command);\n          serviceBlockPacket.skipBits(16);\n        } else {\n          Log.w(TAG, \"Invalid C0 command: \" + command);\n        }\n    }\n  }\n\n  private void handleC1Command(int command) {\n    int window;\n    switch (command) {\n      case COMMAND_CW0:\n      case COMMAND_CW1:\n      case COMMAND_CW2:\n      case COMMAND_CW3:\n      case COMMAND_CW4:\n      case COMMAND_CW5:\n      case COMMAND_CW6:\n      case COMMAND_CW7:\n        window = (command - COMMAND_CW0);\n        if (currentWindow != window) {\n          currentWindow = window;\n          currentCueBuilder = cueBuilders[window];\n        }\n        break;\n      case COMMAND_CLW:\n        for (int i = 1; i <= NUM_WINDOWS; i++) {\n          if (serviceBlockPacket.readBit()) {\n            cueBuilders[NUM_WINDOWS - i].clear();\n          }\n        }\n        break;\n      case COMMAND_DSW:\n        for (int i = 1; i <= NUM_WINDOWS; i++) {\n          if (serviceBlockPacket.readBit()) {\n            cueBuilders[NUM_WINDOWS - i].setVisibility(true);\n          }\n        }\n        break;\n      case COMMAND_HDW:\n        for (int i = 1; i <= NUM_WINDOWS; i++) {\n          if (serviceBlockPacket.readBit()) {\n            cueBuilders[NUM_WINDOWS - i].setVisibility(false);\n          }\n        }\n        break;\n      case COMMAND_TGW:\n        for (int i = 1; i <= NUM_WINDOWS; i++) {\n          if (serviceBlockPacket.readBit()) {\n            CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i];\n            cueBuilder.setVisibility(!cueBuilder.isVisible());\n          }\n        }\n        break;\n      case COMMAND_DLW:\n        for (int i = 1; i <= NUM_WINDOWS; i++) {\n          if (serviceBlockPacket.readBit()) {\n            cueBuilders[NUM_WINDOWS - i].reset();\n          }\n        }\n        break;\n      case COMMAND_DLY:\n        // TODO: Add support for delay commands.\n        serviceBlockPacket.skipBits(8);\n        break;\n      case COMMAND_DLC:\n        // TODO: Add support for delay commands.\n        break;\n      case COMMAND_RST:\n        resetCueBuilders();\n        break;\n      case COMMAND_SPA:\n        if (!currentCueBuilder.isDefined()) {\n          // ignore this command if the current window/cue isn't defined\n          serviceBlockPacket.skipBits(16);\n        } else {\n          handleSetPenAttributes();\n        }\n        break;\n      case COMMAND_SPC:\n        if (!currentCueBuilder.isDefined()) {\n          // ignore this command if the current window/cue isn't defined\n          serviceBlockPacket.skipBits(24);\n        } else {\n          handleSetPenColor();\n        }\n        break;\n      case COMMAND_SPL:\n        if (!currentCueBuilder.isDefined()) {\n          // ignore this command if the current window/cue isn't defined\n          serviceBlockPacket.skipBits(16);\n        } else {\n          handleSetPenLocation();\n        }\n        break;\n      case COMMAND_SWA:\n        if (!currentCueBuilder.isDefined()) {\n          // ignore this command if the current window/cue isn't defined\n          serviceBlockPacket.skipBits(32);\n        } else {\n          handleSetWindowAttributes();\n        }\n        break;\n      case COMMAND_DF0:\n      case COMMAND_DF1:\n      case COMMAND_DF2:\n      case COMMAND_DF3:\n      case COMMAND_DF4:\n      case COMMAND_DF5:\n      case COMMAND_DF6:\n      case COMMAND_DF7:\n        window = (command - COMMAND_DF0);\n        handleDefineWindow(window);\n        // We also set the current window to the newly defined window.\n        if (currentWindow != window) {\n          currentWindow = window;\n          currentCueBuilder = cueBuilders[window];\n        }\n        break;\n      default:\n        Log.w(TAG, \"Invalid C1 command: \" + command);\n    }\n  }\n\n  private void handleC2Command(int command) {\n    // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes\n    if (command <= 0x07) {\n      // Do nothing.\n    } else if (command <= 0x0F) {\n      serviceBlockPacket.skipBits(8);\n    } else if (command <= 0x17) {\n      serviceBlockPacket.skipBits(16);\n    } else if (command <= 0x1F) {\n      serviceBlockPacket.skipBits(24);\n    }\n  }\n\n  private void handleC3Command(int command) {\n    // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes\n    if (command <= 0x87) {\n      serviceBlockPacket.skipBits(32);\n    } else if (command <= 0x8F) {\n      serviceBlockPacket.skipBits(40);\n    } else if (command <= 0x9F) {\n      // 90-9F are variable length codes; the first byte defines the header with the first\n      // 2 bits specifying the type and the last 6 bits specifying the remaining length of the\n      // command in bytes\n      serviceBlockPacket.skipBits(2);\n      int length = serviceBlockPacket.readBits(6);\n      serviceBlockPacket.skipBits(8 * length);\n    }\n  }\n\n  private void handleG0Character(int characterCode) {\n    if (characterCode == CHARACTER_MN) {\n      currentCueBuilder.append('\\u266B');\n    } else {\n      currentCueBuilder.append((char) (characterCode & 0xFF));\n    }\n  }\n\n  private void handleG1Character(int characterCode) {\n    currentCueBuilder.append((char) (characterCode & 0xFF));\n  }\n\n  private void handleG2Character(int characterCode) {\n    switch (characterCode) {\n      case CHARACTER_TSP:\n        currentCueBuilder.append('\\u0020');\n        break;\n      case CHARACTER_NBTSP:\n        currentCueBuilder.append('\\u00A0');\n        break;\n      case CHARACTER_ELLIPSIS:\n        currentCueBuilder.append('\\u2026');\n        break;\n      case CHARACTER_BIG_CARONS:\n        currentCueBuilder.append('\\u0160');\n        break;\n      case CHARACTER_BIG_OE:\n        currentCueBuilder.append('\\u0152');\n        break;\n      case CHARACTER_SOLID_BLOCK:\n        currentCueBuilder.append('\\u2588');\n        break;\n      case CHARACTER_OPEN_SINGLE_QUOTE:\n        currentCueBuilder.append('\\u2018');\n        break;\n      case CHARACTER_CLOSE_SINGLE_QUOTE:\n        currentCueBuilder.append('\\u2019');\n        break;\n      case CHARACTER_OPEN_DOUBLE_QUOTE:\n        currentCueBuilder.append('\\u201C');\n        break;\n      case CHARACTER_CLOSE_DOUBLE_QUOTE:\n        currentCueBuilder.append('\\u201D');\n        break;\n      case CHARACTER_BOLD_BULLET:\n        currentCueBuilder.append('\\u2022');\n        break;\n      case CHARACTER_TM:\n        currentCueBuilder.append('\\u2122');\n        break;\n      case CHARACTER_SMALL_CARONS:\n        currentCueBuilder.append('\\u0161');\n        break;\n      case CHARACTER_SMALL_OE:\n        currentCueBuilder.append('\\u0153');\n        break;\n      case CHARACTER_SM:\n        currentCueBuilder.append('\\u2120');\n        break;\n      case CHARACTER_DIAERESIS_Y:\n        currentCueBuilder.append('\\u0178');\n        break;\n      case CHARACTER_ONE_EIGHTH:\n        currentCueBuilder.append('\\u215B');\n        break;\n      case CHARACTER_THREE_EIGHTHS:\n        currentCueBuilder.append('\\u215C');\n        break;\n      case CHARACTER_FIVE_EIGHTHS:\n        currentCueBuilder.append('\\u215D');\n        break;\n      case CHARACTER_SEVEN_EIGHTHS:\n        currentCueBuilder.append('\\u215E');\n        break;\n      case CHARACTER_VERTICAL_BORDER:\n        currentCueBuilder.append('\\u2502');\n        break;\n      case CHARACTER_UPPER_RIGHT_BORDER:\n        currentCueBuilder.append('\\u2510');\n        break;\n      case CHARACTER_LOWER_LEFT_BORDER:\n        currentCueBuilder.append('\\u2514');\n        break;\n      case CHARACTER_HORIZONTAL_BORDER:\n        currentCueBuilder.append('\\u2500');\n        break;\n      case CHARACTER_LOWER_RIGHT_BORDER:\n        currentCueBuilder.append('\\u2518');\n        break;\n      case CHARACTER_UPPER_LEFT_BORDER:\n        currentCueBuilder.append('\\u250C');\n        break;\n      default:\n        Log.w(TAG, \"Invalid G2 character: \" + characterCode);\n        // The CEA-708 specification doesn't specify what to do in the case of an unexpected\n        // value in the G2 character range, so we ignore it.\n    }\n  }\n\n  private void handleG3Character(int characterCode) {\n    if (characterCode == 0xA0) {\n      currentCueBuilder.append('\\u33C4');\n    } else {\n      Log.w(TAG, \"Invalid G3 character: \" + characterCode);\n      // Substitute any unsupported G3 character with an underscore as per CEA-708 specification.\n      currentCueBuilder.append('_');\n    }\n  }\n\n  private void handleSetPenAttributes() {\n    // the SetPenAttributes command contains 2 bytes of data\n    // first byte\n    int textTag = serviceBlockPacket.readBits(4);\n    int offset = serviceBlockPacket.readBits(2);\n    int penSize = serviceBlockPacket.readBits(2);\n    // second byte\n    boolean italicsToggle = serviceBlockPacket.readBit();\n    boolean underlineToggle = serviceBlockPacket.readBit();\n    int edgeType = serviceBlockPacket.readBits(3);\n    int fontStyle = serviceBlockPacket.readBits(3);\n\n    currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle,\n        edgeType, fontStyle);\n  }\n\n  private void handleSetPenColor() {\n    // the SetPenColor command contains 3 bytes of data\n    // first byte\n    int foregroundO = serviceBlockPacket.readBits(2);\n    int foregroundR = serviceBlockPacket.readBits(2);\n    int foregroundG = serviceBlockPacket.readBits(2);\n    int foregroundB = serviceBlockPacket.readBits(2);\n    int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB,\n        foregroundO);\n    // second byte\n    int backgroundO = serviceBlockPacket.readBits(2);\n    int backgroundR = serviceBlockPacket.readBits(2);\n    int backgroundG = serviceBlockPacket.readBits(2);\n    int backgroundB = serviceBlockPacket.readBits(2);\n    int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB,\n        backgroundO);\n    // third byte\n    serviceBlockPacket.skipBits(2); // null padding\n    int edgeR = serviceBlockPacket.readBits(2);\n    int edgeG = serviceBlockPacket.readBits(2);\n    int edgeB = serviceBlockPacket.readBits(2);\n    int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB);\n\n    currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);\n  }\n\n  private void handleSetPenLocation() {\n    // the SetPenLocation command contains 2 bytes of data\n    // first byte\n    serviceBlockPacket.skipBits(4);\n    int row = serviceBlockPacket.readBits(4);\n    // second byte\n    serviceBlockPacket.skipBits(2);\n    int column = serviceBlockPacket.readBits(6);\n\n    currentCueBuilder.setPenLocation(row, column);\n  }\n\n  private void handleSetWindowAttributes() {\n    // the SetWindowAttributes command contains 4 bytes of data\n    // first byte\n    int fillO = serviceBlockPacket.readBits(2);\n    int fillR = serviceBlockPacket.readBits(2);\n    int fillG = serviceBlockPacket.readBits(2);\n    int fillB = serviceBlockPacket.readBits(2);\n    int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO);\n    // second byte\n    int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType\n    int borderR = serviceBlockPacket.readBits(2);\n    int borderG = serviceBlockPacket.readBits(2);\n    int borderB = serviceBlockPacket.readBits(2);\n    int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB);\n    // third byte\n    if (serviceBlockPacket.readBit()) {\n      borderType |= 0x04; // set the top bit of the 3-bit borderType\n    }\n    boolean wordWrapToggle = serviceBlockPacket.readBit();\n    int printDirection = serviceBlockPacket.readBits(2);\n    int scrollDirection = serviceBlockPacket.readBits(2);\n    int justification = serviceBlockPacket.readBits(2);\n    // fourth byte\n    // Note that we don't intend to support display effects\n    serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2)\n\n    currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType,\n        printDirection, scrollDirection, justification);\n  }\n\n  private void handleDefineWindow(int window) {\n    CueBuilder cueBuilder = cueBuilders[window];\n\n    // the DefineWindow command contains 6 bytes of data\n    // first byte\n    serviceBlockPacket.skipBits(2); // null padding\n    boolean visible = serviceBlockPacket.readBit();\n    boolean rowLock = serviceBlockPacket.readBit();\n    boolean columnLock = serviceBlockPacket.readBit();\n    int priority = serviceBlockPacket.readBits(3);\n    // second byte\n    boolean relativePositioning = serviceBlockPacket.readBit();\n    int verticalAnchor = serviceBlockPacket.readBits(7);\n    // third byte\n    int horizontalAnchor = serviceBlockPacket.readBits(8);\n    // fourth byte\n    int anchorId = serviceBlockPacket.readBits(4);\n    int rowCount = serviceBlockPacket.readBits(4);\n    // fifth byte\n    serviceBlockPacket.skipBits(2); // null padding\n    int columnCount = serviceBlockPacket.readBits(6);\n    // sixth byte\n    serviceBlockPacket.skipBits(2); // null padding\n    int windowStyle = serviceBlockPacket.readBits(3);\n    int penStyle = serviceBlockPacket.readBits(3);\n\n    cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning,\n        verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle);\n  }\n\n  private List<Cue> getDisplayCues() {\n    List<Cea708Cue> displayCues = new ArrayList<>();\n    for (int i = 0; i < NUM_WINDOWS; i++) {\n      if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) {\n        displayCues.add(cueBuilders[i].build());\n      }\n    }\n    Collections.sort(displayCues);\n    return Collections.unmodifiableList(displayCues);\n  }\n\n  private void resetCueBuilders() {\n    for (int i = 0; i < NUM_WINDOWS; i++) {\n      cueBuilders[i].reset();\n    }\n  }\n\n  private static final class DtvCcPacket {\n\n    public final int sequenceNumber;\n    public final int packetSize;\n    public final byte[] packetData;\n\n    int currentIndex;\n\n    public DtvCcPacket(int sequenceNumber, int packetSize) {\n      this.sequenceNumber = sequenceNumber;\n      this.packetSize = packetSize;\n      packetData = new byte[2 * packetSize - 1];\n      currentIndex = 0;\n    }\n\n  }\n\n  // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder\n  // which could be refactored into a separate class.\n  private static final class CueBuilder {\n\n    private static final int RELATIVE_CUE_SIZE = 99;\n    private static final int VERTICAL_SIZE = 74;\n    private static final int HORIZONTAL_SIZE = 209;\n\n    private static final int DEFAULT_PRIORITY = 4;\n\n    private static final int MAXIMUM_ROW_COUNT = 15;\n\n    private static final int JUSTIFICATION_LEFT = 0;\n    private static final int JUSTIFICATION_RIGHT = 1;\n    private static final int JUSTIFICATION_CENTER = 2;\n    private static final int JUSTIFICATION_FULL = 3;\n\n    private static final int DIRECTION_LEFT_TO_RIGHT = 0;\n    private static final int DIRECTION_RIGHT_TO_LEFT = 1;\n    private static final int DIRECTION_TOP_TO_BOTTOM = 2;\n    private static final int DIRECTION_BOTTOM_TO_TOP = 3;\n\n    // TODO: Add other border/edge types when utilized.\n    private static final int BORDER_AND_EDGE_TYPE_NONE = 0;\n    private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;\n\n    public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);\n    public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);\n    public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);\n\n    // TODO: Add other sizes when utilized.\n    private static final int PEN_SIZE_STANDARD = 1;\n\n    // TODO: Add other pen font styles when utilized.\n    private static final int PEN_FONT_STYLE_DEFAULT = 0;\n    private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;\n    private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;\n    private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;\n    private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;\n\n    // TODO: Add other pen offsets when utilized.\n    private static final int PEN_OFFSET_NORMAL = 1;\n\n    // The window style properties are specified in the CEA-708 specification.\n    private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] {\n        JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,\n        JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,\n        JUSTIFICATION_LEFT\n    };\n    private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] {\n        DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,\n        DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,\n        DIRECTION_TOP_TO_BOTTOM\n    };\n    private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] {\n        DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,\n        DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,\n        DIRECTION_RIGHT_TO_LEFT\n    };\n    private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] {\n        false, false, false, true, true, true, false\n    };\n    private static final int[] WINDOW_STYLE_FILL = new int[] {\n        COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,\n        COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK\n    };\n\n    // The pen style properties are specified in the CEA-708 specification.\n    private static final int[] PEN_STYLE_FONT_STYLE = new int[] {\n        PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,\n        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,\n        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,\n        PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,\n        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS\n    };\n    private static final int[] PEN_STYLE_EDGE_TYPE = new int[] {\n        BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,\n        BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,\n        BORDER_AND_EDGE_TYPE_UNIFORM\n    };\n    private static final int[] PEN_STYLE_BACKGROUND = new int[] {\n        COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,\n        COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};\n\n    private final List<SpannableString> rolledUpCaptions;\n    private final SpannableStringBuilder captionStringBuilder;\n\n    // Window/Cue properties\n    private boolean defined;\n    private boolean visible;\n    private int priority;\n    private boolean relativePositioning;\n    private int verticalAnchor;\n    private int horizontalAnchor;\n    private int anchorId;\n    private int rowCount;\n    private boolean rowLock;\n    private int justification;\n    private int windowStyleId;\n    private int penStyleId;\n    private int windowFillColor;\n\n    // Pen/Text properties\n    private int italicsStartPosition;\n    private int underlineStartPosition;\n    private int foregroundColorStartPosition;\n    private int foregroundColor;\n    private int backgroundColorStartPosition;\n    private int backgroundColor;\n    private int row;\n\n    public CueBuilder() {\n      rolledUpCaptions = new ArrayList<>();\n      captionStringBuilder = new SpannableStringBuilder();\n      reset();\n    }\n\n    public boolean isEmpty() {\n      return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);\n    }\n\n    public void reset() {\n      clear();\n\n      defined = false;\n      visible = false;\n      priority = DEFAULT_PRIORITY;\n      relativePositioning = false;\n      verticalAnchor = 0;\n      horizontalAnchor = 0;\n      anchorId = 0;\n      rowCount = MAXIMUM_ROW_COUNT;\n      rowLock = true;\n      justification = JUSTIFICATION_LEFT;\n      windowStyleId = 0;\n      penStyleId = 0;\n      windowFillColor = COLOR_SOLID_BLACK;\n\n      foregroundColor = COLOR_SOLID_WHITE;\n      backgroundColor = COLOR_SOLID_BLACK;\n    }\n\n    public void clear() {\n      rolledUpCaptions.clear();\n      captionStringBuilder.clear();\n      italicsStartPosition = C.POSITION_UNSET;\n      underlineStartPosition = C.POSITION_UNSET;\n      foregroundColorStartPosition = C.POSITION_UNSET;\n      backgroundColorStartPosition = C.POSITION_UNSET;\n      row = 0;\n    }\n\n    public boolean isDefined() {\n      return defined;\n    }\n\n    public void setVisibility(boolean visible) {\n      this.visible = visible;\n    }\n\n    public boolean isVisible() {\n      return visible;\n    }\n\n    public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,\n        boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,\n        int columnCount, int anchorId, int windowStyleId, int penStyleId) {\n      this.defined = true;\n      this.visible = visible;\n      this.rowLock = rowLock;\n      this.priority = priority;\n      this.relativePositioning = relativePositioning;\n      this.verticalAnchor = verticalAnchor;\n      this.horizontalAnchor = horizontalAnchor;\n      this.anchorId = anchorId;\n\n      // Decoders must add one to rowCount to get the desired number of rows.\n      if (this.rowCount != rowCount + 1) {\n        this.rowCount = rowCount + 1;\n\n        // Trim any rolled up captions that are no longer valid, if applicable.\n        while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))\n            || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {\n          rolledUpCaptions.remove(0);\n        }\n      }\n\n      // TODO: Add support for column lock and count.\n\n      if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {\n        this.windowStyleId = windowStyleId;\n        // windowStyleId is 1-based.\n        int windowStyleIdIndex = windowStyleId - 1;\n        // Note that Border type and border color are the same for all window styles.\n        setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,\n            WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,\n            WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],\n            WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],\n            WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);\n      }\n\n      if (penStyleId != 0 && this.penStyleId != penStyleId) {\n        this.penStyleId = penStyleId;\n        // penStyleId is 1-based.\n        int penStyleIdIndex = penStyleId - 1;\n        // Note that pen size, offset, italics, underline, foreground color, and foreground\n        // opacity are the same for all pen styles.\n        setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,\n            PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);\n        setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);\n      }\n    }\n\n\n    public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,\n        int borderType, int printDirection, int scrollDirection, int justification) {\n      this.windowFillColor = fillColor;\n      // TODO: Add support for border color and types.\n      // TODO: Add support for word wrap.\n      // TODO: Add support for other scroll directions.\n      // TODO: Add support for other print directions.\n      this.justification = justification;\n\n    }\n\n    public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,\n        boolean underlineToggle, int edgeType, int fontStyle) {\n      // TODO: Add support for text tags.\n      // TODO: Add support for other offsets.\n      // TODO: Add support for other pen sizes.\n\n      if (italicsStartPosition != C.POSITION_UNSET) {\n        if (!italicsToggle) {\n          captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,\n              captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n          italicsStartPosition = C.POSITION_UNSET;\n        }\n      } else if (italicsToggle) {\n        italicsStartPosition = captionStringBuilder.length();\n      }\n\n      if (underlineStartPosition != C.POSITION_UNSET) {\n        if (!underlineToggle) {\n          captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,\n              captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n          underlineStartPosition = C.POSITION_UNSET;\n        }\n      } else if (underlineToggle) {\n        underlineStartPosition = captionStringBuilder.length();\n      }\n\n      // TODO: Add support for edge types.\n      // TODO: Add support for other font styles.\n    }\n\n    public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {\n      if (foregroundColorStartPosition != C.POSITION_UNSET) {\n        if (this.foregroundColor != foregroundColor) {\n          captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),\n              foregroundColorStartPosition, captionStringBuilder.length(),\n              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n      }\n      if (foregroundColor != COLOR_SOLID_WHITE) {\n        foregroundColorStartPosition = captionStringBuilder.length();\n        this.foregroundColor = foregroundColor;\n      }\n\n      if (backgroundColorStartPosition != C.POSITION_UNSET) {\n        if (this.backgroundColor != backgroundColor) {\n          captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),\n              backgroundColorStartPosition, captionStringBuilder.length(),\n              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n      }\n      if (backgroundColor != COLOR_SOLID_BLACK) {\n        backgroundColorStartPosition = captionStringBuilder.length();\n        this.backgroundColor = backgroundColor;\n      }\n\n      // TODO: Add support for edge color.\n    }\n\n    public void setPenLocation(int row, int column) {\n      // TODO: Support moving the pen location with a window properly.\n\n      // Until we support proper pen locations, if we encounter a row that's different from the\n      // previous one, we should append a new line. Otherwise, we'll see strings that should be\n      // on new lines concatenated with the previous, resulting in 2 words being combined, as\n      // well as potentially drawing beyond the width of the window/screen.\n      if (this.row != row) {\n        append('\\n');\n      }\n      this.row = row;\n    }\n\n    public void backspace() {\n      int length = captionStringBuilder.length();\n      if (length > 0) {\n        captionStringBuilder.delete(length - 1, length);\n      }\n    }\n\n    public void append(char text) {\n      if (text == '\\n') {\n        rolledUpCaptions.add(buildSpannableString());\n        captionStringBuilder.clear();\n\n        if (italicsStartPosition != C.POSITION_UNSET) {\n          italicsStartPosition = 0;\n        }\n        if (underlineStartPosition != C.POSITION_UNSET) {\n          underlineStartPosition = 0;\n        }\n        if (foregroundColorStartPosition != C.POSITION_UNSET) {\n          foregroundColorStartPosition = 0;\n        }\n        if (backgroundColorStartPosition != C.POSITION_UNSET) {\n          backgroundColorStartPosition = 0;\n        }\n\n        while ((rowLock && (rolledUpCaptions.size() >= rowCount))\n            || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {\n          rolledUpCaptions.remove(0);\n        }\n      } else {\n        captionStringBuilder.append(text);\n      }\n    }\n\n    public SpannableString buildSpannableString() {\n      SpannableStringBuilder spannableStringBuilder =\n          new SpannableStringBuilder(captionStringBuilder);\n      int length = spannableStringBuilder.length();\n\n      if (length > 0) {\n        if (italicsStartPosition != C.POSITION_UNSET) {\n          spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,\n              length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n\n        if (underlineStartPosition != C.POSITION_UNSET) {\n          spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,\n              length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n\n        if (foregroundColorStartPosition != C.POSITION_UNSET) {\n          spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),\n              foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n\n        if (backgroundColorStartPosition != C.POSITION_UNSET) {\n          spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),\n              backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n      }\n\n      return new SpannableString(spannableStringBuilder);\n    }\n\n    public Cea708Cue build() {\n      if (isEmpty()) {\n        // The cue is empty.\n        return null;\n      }\n\n      SpannableStringBuilder cueString = new SpannableStringBuilder();\n\n      // Add any rolled up captions, separated by new lines.\n      for (int i = 0; i < rolledUpCaptions.size(); i++) {\n        cueString.append(rolledUpCaptions.get(i));\n        cueString.append('\\n');\n      }\n      // Add the current line.\n      cueString.append(buildSpannableString());\n\n      // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal\n      // alignment).\n      Alignment alignment;\n      switch (justification) {\n        case JUSTIFICATION_FULL:\n          // TODO: Add support for full justification.\n        case JUSTIFICATION_LEFT:\n          alignment = Alignment.ALIGN_NORMAL;\n          break;\n        case JUSTIFICATION_RIGHT:\n          alignment = Alignment.ALIGN_OPPOSITE;\n          break;\n        case JUSTIFICATION_CENTER:\n          alignment = Alignment.ALIGN_CENTER;\n          break;\n        default:\n          throw new IllegalArgumentException(\"Unexpected justification value: \" + justification);\n      }\n\n      float position;\n      float line;\n      if (relativePositioning) {\n        position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;\n        line = (float) verticalAnchor / RELATIVE_CUE_SIZE;\n      } else {\n        position = (float) horizontalAnchor / HORIZONTAL_SIZE;\n        line = (float) verticalAnchor / VERTICAL_SIZE;\n      }\n      // Apply screen-edge padding to the line and position.\n      position = (position * 0.9f) + 0.05f;\n      line = (line * 0.9f) + 0.05f;\n\n      // anchorId specifies where the anchor should be placed on the caption cue/window. The 9\n      // possible configurations are as follows:\n      //   0-----1-----2\n      //   |           |\n      //   3     4     5\n      //   |           |\n      //   6-----7-----8\n      @AnchorType int verticalAnchorType;\n      if (anchorId % 3 == 0) {\n        verticalAnchorType = Cue.ANCHOR_TYPE_START;\n      } else if (anchorId % 3 == 1) {\n        verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;\n      } else {\n        verticalAnchorType = Cue.ANCHOR_TYPE_END;\n      }\n      // TODO: Add support for right-to-left languages (i.e. where start is on the right).\n      @AnchorType int horizontalAnchorType;\n      if (anchorId / 3 == 0) {\n        horizontalAnchorType = Cue.ANCHOR_TYPE_START;\n      } else if (anchorId / 3 == 1) {\n        horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;\n      } else {\n        horizontalAnchorType = Cue.ANCHOR_TYPE_END;\n      }\n\n      boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);\n\n      return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType,\n          position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor,\n          priority);\n    }\n\n    public static int getArgbColorFromCeaColor(int red, int green, int blue) {\n      return getArgbColorFromCeaColor(red, green, blue, 0);\n    }\n\n    public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {\n      Assertions.checkIndex(red, 0, 4);\n      Assertions.checkIndex(green, 0, 4);\n      Assertions.checkIndex(blue, 0, 4);\n      Assertions.checkIndex(opacity, 0, 4);\n\n      int alpha;\n      switch (opacity) {\n        case 0:\n        case 1:\n          // Note the value of '1' is actually FLASH, but we don't support that.\n          alpha = 255;\n          break;\n        case 2:\n          alpha = 127;\n          break;\n        case 3:\n          alpha = 0;\n          break;\n        default:\n          alpha = 255;\n      }\n\n      // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.\n\n      // Return values based on the Minimum Color List\n      return Color.argb(alpha,\n          (red > 1 ? 255 : 0),\n          (green > 1 ? 255 : 0),\n          (blue > 1 ? 255 : 0));\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/** Initialization data for CEA-708 decoders. */\npublic final class Cea708InitializationData {\n\n  /**\n   * Whether the closed caption service is formatted for displays with 16:9 aspect ratio. If false,\n   * the closed caption service is formatted for 4:3 displays.\n   */\n  public final boolean isWideAspectRatio;\n\n  private Cea708InitializationData(List<byte[]> initializationData) {\n    isWideAspectRatio = initializationData.get(0)[0] != 0;\n  }\n\n  /**\n   * Returns an object representation of CEA-708 initialization data\n   *\n   * @param initializationData Binary CEA-708 initialization data.\n   * @return The object representation.\n   */\n  public static Cea708InitializationData fromData(List<byte[]> initializationData) {\n    return new Cea708InitializationData(initializationData);\n  }\n\n  /**\n   * Builds binary CEA-708 initialization data.\n   *\n   * @param isWideAspectRatio Whether the closed caption service is formatted for displays with 16:9\n   *     aspect ratio.\n   * @return Binary CEA-708 initializaton data.\n   */\n  public static List<byte[]> buildData(boolean isWideAspectRatio) {\n    return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)});\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport androidx.annotation.NonNull;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.text.SubtitleInputBuffer;\nimport com.google.android.exoplayer2.text.SubtitleOutputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayDeque;\nimport java.util.PriorityQueue;\n\n/**\n * Base class for subtitle parsers for CEA captions.\n */\n/* package */ abstract class CeaDecoder implements SubtitleDecoder {\n\n  private static final int NUM_INPUT_BUFFERS = 10;\n  private static final int NUM_OUTPUT_BUFFERS = 2;\n\n  private final ArrayDeque<CeaInputBuffer> availableInputBuffers;\n  private final ArrayDeque<SubtitleOutputBuffer> availableOutputBuffers;\n  private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;\n\n  private CeaInputBuffer dequeuedInputBuffer;\n  private long playbackPositionUs;\n  private long queuedInputBufferCount;\n\n  public CeaDecoder() {\n    availableInputBuffers = new ArrayDeque<>();\n    for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {\n      availableInputBuffers.add(new CeaInputBuffer());\n    }\n    availableOutputBuffers = new ArrayDeque<>();\n    for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {\n      availableOutputBuffers.add(new CeaOutputBuffer());\n    }\n    queuedInputBuffers = new PriorityQueue<>();\n  }\n\n  @Override\n  public abstract String getName();\n\n  @Override\n  public void setPositionUs(long positionUs) {\n    playbackPositionUs = positionUs;\n  }\n\n  @Override\n  public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {\n    Assertions.checkState(dequeuedInputBuffer == null);\n    if (availableInputBuffers.isEmpty()) {\n      return null;\n    }\n    dequeuedInputBuffer = availableInputBuffers.pollFirst();\n    return dequeuedInputBuffer;\n  }\n\n  @Override\n  public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {\n    Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);\n    if (inputBuffer.isDecodeOnly()) {\n      // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow\n      // for decoding to begin mid-stream.\n      releaseInputBuffer(dequeuedInputBuffer);\n    } else {\n      dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++;\n      queuedInputBuffers.add(dequeuedInputBuffer);\n    }\n    dequeuedInputBuffer = null;\n  }\n\n  @Override\n  public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {\n    if (availableOutputBuffers.isEmpty()) {\n      return null;\n    }\n    // iterate through all available input buffers whose timestamps are less than or equal\n    // to the current playback position; processing input buffers for future content should\n    // be deferred until they would be applicable\n    while (!queuedInputBuffers.isEmpty()\n        && queuedInputBuffers.peek().timeUs <= playbackPositionUs) {\n      CeaInputBuffer inputBuffer = queuedInputBuffers.poll();\n\n      // If the input buffer indicates we've reached the end of the stream, we can\n      // return immediately with an output buffer propagating that\n      if (inputBuffer.isEndOfStream()) {\n        SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();\n        outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);\n        releaseInputBuffer(inputBuffer);\n        return outputBuffer;\n      }\n\n      decode(inputBuffer);\n\n      // check if we have any caption updates to report\n      if (isNewSubtitleDataAvailable()) {\n        // Even if the subtitle is decode-only; we need to generate it to consume the data so it\n        // isn't accidentally prepended to the next subtitle\n        Subtitle subtitle = createSubtitle();\n        if (!inputBuffer.isDecodeOnly()) {\n          SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();\n          outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE);\n          releaseInputBuffer(inputBuffer);\n          return outputBuffer;\n        }\n      }\n\n      releaseInputBuffer(inputBuffer);\n    }\n\n    return null;\n  }\n\n  private void releaseInputBuffer(CeaInputBuffer inputBuffer) {\n    inputBuffer.clear();\n    availableInputBuffers.add(inputBuffer);\n  }\n\n  protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {\n    outputBuffer.clear();\n    availableOutputBuffers.add(outputBuffer);\n  }\n\n  @Override\n  public void flush() {\n    queuedInputBufferCount = 0;\n    playbackPositionUs = 0;\n    while (!queuedInputBuffers.isEmpty()) {\n      releaseInputBuffer(queuedInputBuffers.poll());\n    }\n    if (dequeuedInputBuffer != null) {\n      releaseInputBuffer(dequeuedInputBuffer);\n      dequeuedInputBuffer = null;\n    }\n  }\n\n  @Override\n  public void release() {\n    // Do nothing\n  }\n\n  /**\n   * Returns whether there is data available to create a new {@link Subtitle}.\n   */\n  protected abstract boolean isNewSubtitleDataAvailable();\n\n  /**\n   * Creates a {@link Subtitle} from the available data.\n   */\n  protected abstract Subtitle createSubtitle();\n\n  /**\n   * Filters and processes the raw data, providing {@link Subtitle}s via {@link #createSubtitle()}\n   * when sufficient data has been processed.\n   */\n  protected abstract void decode(SubtitleInputBuffer inputBuffer);\n\n  private static final class CeaInputBuffer extends SubtitleInputBuffer\n      implements Comparable<CeaInputBuffer> {\n\n    private long queuedInputBufferCount;\n\n    @Override\n    public int compareTo(@NonNull CeaInputBuffer other) {\n      if (isEndOfStream() != other.isEndOfStream()) {\n        return isEndOfStream() ? 1 : -1;\n      }\n      long delta = timeUs - other.timeUs;\n      if (delta == 0) {\n        delta = queuedInputBufferCount - other.queuedInputBufferCount;\n        if (delta == 0) {\n          return 0;\n        }\n      }\n      return delta > 0 ? 1 : -1;\n    }\n  }\n\n  private final class CeaOutputBuffer extends SubtitleOutputBuffer {\n\n    @Override\n    public final void release() {\n      releaseOutputBuffer(this);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A representation of a CEA subtitle.\n */\n/* package */ final class CeaSubtitle implements Subtitle {\n\n  private final List<Cue> cues;\n\n  /**\n   * @param cues The subtitle cues.\n   */\n  public CeaSubtitle(List<Cue> cues) {\n    this.cues = cues;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return timeUs < 0 ? 0 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return 1;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index == 0);\n    return 0;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return timeUs >= 0 ? cues : Collections.emptyList();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.cea;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */\npublic final class CeaUtil {\n\n  private static final String TAG = \"CeaUtil\";\n\n  public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934;\n  public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3;\n\n  private static final int PAYLOAD_TYPE_CC = 4;\n  private static final int COUNTRY_CODE = 0xB5;\n  private static final int PROVIDER_CODE_ATSC = 0x31;\n  private static final int PROVIDER_CODE_DIRECTV = 0x2F;\n\n  /**\n   * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages\n   * as samples to all of the provided outputs.\n   *\n   * @param presentationTimeUs The presentation time in microseconds for any samples.\n   * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.\n   * @param outputs The outputs to which any samples should be written.\n   */\n  public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,\n      TrackOutput[] outputs) {\n    while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {\n      int payloadType = readNon255TerminatedValue(seiBuffer);\n      int payloadSize = readNon255TerminatedValue(seiBuffer);\n      int nextPayloadPosition = seiBuffer.getPosition() + payloadSize;\n      // Process the payload.\n      if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) {\n        // This might occur if we're trying to read an encrypted SEI NAL unit.\n        Log.w(TAG, \"Skipping remainder of malformed SEI NAL unit.\");\n        nextPayloadPosition = seiBuffer.limit();\n      } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) {\n        int countryCode = seiBuffer.readUnsignedByte();\n        int providerCode = seiBuffer.readUnsignedShort();\n        int userIdentifier = 0;\n        if (providerCode == PROVIDER_CODE_ATSC) {\n          userIdentifier = seiBuffer.readInt();\n        }\n        int userDataTypeCode = seiBuffer.readUnsignedByte();\n        if (providerCode == PROVIDER_CODE_DIRECTV) {\n          seiBuffer.skipBytes(1); // user_data_length.\n        }\n        boolean messageIsSupportedCeaCaption =\n            countryCode == COUNTRY_CODE\n                && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV)\n                && userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC;\n        if (providerCode == PROVIDER_CODE_ATSC) {\n          messageIsSupportedCeaCaption &= userIdentifier == USER_DATA_IDENTIFIER_GA94;\n        }\n        if (messageIsSupportedCeaCaption) {\n          consumeCcData(presentationTimeUs, seiBuffer, outputs);\n        }\n      }\n      seiBuffer.setPosition(nextPayloadPosition);\n    }\n  }\n\n  /**\n   * Consumes caption data (cc_data), writing the content as samples to all of the provided outputs.\n   *\n   * @param presentationTimeUs The presentation time in microseconds for any samples.\n   * @param ccDataBuffer The buffer containing the caption data.\n   * @param outputs The outputs to which any samples should be written.\n   */\n  public static void consumeCcData(\n      long presentationTimeUs, ParsableByteArray ccDataBuffer, TrackOutput[] outputs) {\n    // First byte contains: reserved (1), process_cc_data_flag (1), zero_bit (1), cc_count (5).\n    int firstByte = ccDataBuffer.readUnsignedByte();\n    boolean processCcDataFlag = (firstByte & 0x40) != 0;\n    if (!processCcDataFlag) {\n      // No need to process.\n      return;\n    }\n    int ccCount = firstByte & 0x1F;\n    ccDataBuffer.skipBytes(1); // Ignore em_data\n    // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)\n    // + cc_data_1 (8) + cc_data_2 (8).\n    int sampleLength = ccCount * 3;\n    int sampleStartPosition = ccDataBuffer.getPosition();\n    for (TrackOutput output : outputs) {\n      ccDataBuffer.setPosition(sampleStartPosition);\n      output.sampleData(ccDataBuffer, sampleLength);\n      output.sampleMetadata(\n          presentationTimeUs,\n          C.BUFFER_FLAG_KEY_FRAME,\n          sampleLength,\n          /* offset= */ 0,\n          /* encryptionData= */ null);\n    }\n  }\n\n  /**\n   * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a\n   * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the\n   * number of 0xFF bytes and T is the value of the terminating byte.\n   *\n   * @param buffer The buffer from which to read the value.\n   * @return The read value, or -1 if the end of the buffer is reached before a value is read.\n   */\n  private static int readNon255TerminatedValue(ParsableByteArray buffer) {\n    int b;\n    int value = 0;\n    do {\n      if (buffer.bytesLeft() == 0) {\n        return -1;\n      }\n      b = buffer.readUnsignedByte();\n      value += b;\n    } while (b == 0xFF);\n    return value;\n  }\n\n  private CeaUtil() {}\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.cea;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.dvb;\n\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.List;\n\n/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */\npublic final class DvbDecoder extends SimpleSubtitleDecoder {\n\n  private final DvbParser parser;\n\n  /**\n   * @param initializationData The initialization data for the decoder. The initialization data\n   *     must consist of a single byte array containing 5 bytes: flag_pes_stripped (1),\n   *     composition_page (2), ancillary_page (2).\n   */\n  public DvbDecoder(List<byte[]> initializationData) {\n    super(\"DvbDecoder\");\n    ParsableByteArray data = new ParsableByteArray(initializationData.get(0));\n    int subtitleCompositionPage = data.readUnsignedShort();\n    int subtitleAncillaryPage = data.readUnsignedShort();\n    parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage);\n  }\n\n  @Override\n  protected Subtitle decode(byte[] data, int length, boolean reset) {\n    if (reset) {\n      parser.reset();\n    }\n    return new DvbSubtitle(parser.decode(data, length));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.dvb;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.PorterDuff;\nimport android.graphics.PorterDuffXfermode;\nimport android.util.SparseArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * Parses {@link Cue}s from a DVB subtitle bitstream.\n */\n/* package */ final class DvbParser {\n\n  private static final String TAG = \"DvbParser\";\n\n  // Segment types, as defined by ETSI EN 300 743 Table 2\n  private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10;\n  private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11;\n  private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12;\n  private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13;\n  private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14;\n\n  // Page states, as defined by ETSI EN 300 743 Table 3\n  private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements.\n  // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements.\n  // private static final int PAGE_STATE_CHANGE = 2; // New. All elements.\n\n  // Region depths, as defined by ETSI EN 300 743 Table 5\n  // private static final int REGION_DEPTH_2_BIT = 1;\n  private static final int REGION_DEPTH_4_BIT = 2;\n  private static final int REGION_DEPTH_8_BIT = 3;\n\n  // Object codings, as defined by ETSI EN 300 743 Table 8\n  private static final int OBJECT_CODING_PIXELS = 0;\n  private static final int OBJECT_CODING_STRING = 1;\n\n  // Pixel-data types, as defined by ETSI EN 300 743 Table 9\n  private static final int DATA_TYPE_2BP_CODE_STRING = 0x10;\n  private static final int DATA_TYPE_4BP_CODE_STRING = 0x11;\n  private static final int DATA_TYPE_8BP_CODE_STRING = 0x12;\n  private static final int DATA_TYPE_24_TABLE_DATA = 0x20;\n  private static final int DATA_TYPE_28_TABLE_DATA = 0x21;\n  private static final int DATA_TYPE_48_TABLE_DATA = 0x22;\n  private static final int DATA_TYPE_END_LINE = 0xF0;\n\n  // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6\n  private static final byte[] defaultMap2To4 = {\n      (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F};\n  private static final byte[] defaultMap2To8 = {\n      (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF};\n  private static final byte[] defaultMap4To8 = {\n      (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,\n      (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,\n      (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB,\n      (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF};\n\n  private final Paint defaultPaint;\n  private final Paint fillRegionPaint;\n  private final Canvas canvas;\n  private final DisplayDefinition defaultDisplayDefinition;\n  private final ClutDefinition defaultClutDefinition;\n  private final SubtitleService subtitleService;\n\n  @MonotonicNonNull private Bitmap bitmap;\n\n  /**\n   * Construct an instance for the given subtitle and ancillary page ids.\n   *\n   * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed.\n   * @param ancillaryPageId The id of the ancillary page containing additional data.\n   */\n  public DvbParser(int subtitlePageId, int ancillaryPageId) {\n    defaultPaint = new Paint();\n    defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE);\n    defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));\n    defaultPaint.setPathEffect(null);\n    fillRegionPaint = new Paint();\n    fillRegionPaint.setStyle(Paint.Style.FILL);\n    fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));\n    fillRegionPaint.setPathEffect(null);\n    canvas = new Canvas();\n    defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575);\n    defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(),\n        generateDefault4BitClutEntries(), generateDefault8BitClutEntries());\n    subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId);\n  }\n\n  /**\n   * Resets the parser.\n   */\n  public void reset() {\n    subtitleService.reset();\n  }\n\n  /**\n   * Decodes a subtitling packet, returning a list of parsed {@link Cue}s.\n   *\n   * @param data The subtitling packet data to decode.\n   * @param limit The limit in {@code data} at which to stop decoding.\n   * @return The parsed {@link Cue}s.\n   */\n  public List<Cue> decode(byte[] data, int limit) {\n    // Parse the input data.\n    ParsableBitArray dataBitArray = new ParsableBitArray(data, limit);\n    while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40)\n        && dataBitArray.readBits(8) == 0x0F) {\n      parseSubtitlingSegment(dataBitArray, subtitleService);\n    }\n\n    @Nullable PageComposition pageComposition = subtitleService.pageComposition;\n    if (pageComposition == null) {\n      return Collections.emptyList();\n    }\n\n    // Update the canvas bitmap if necessary.\n    DisplayDefinition displayDefinition = subtitleService.displayDefinition != null\n        ? subtitleService.displayDefinition : defaultDisplayDefinition;\n    if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth()\n        || displayDefinition.height + 1 != bitmap.getHeight()) {\n      bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1,\n          Bitmap.Config.ARGB_8888);\n      canvas.setBitmap(bitmap);\n    }\n\n    // Build the cues.\n    List<Cue> cues = new ArrayList<>();\n    SparseArray<PageRegion> pageRegions = pageComposition.regions;\n    for (int i = 0; i < pageRegions.size(); i++) {\n      // Save clean clipping state.\n      canvas.save();\n      PageRegion pageRegion = pageRegions.valueAt(i);\n      int regionId = pageRegions.keyAt(i);\n      RegionComposition regionComposition = subtitleService.regions.get(regionId);\n\n      // Clip drawing to the current region and display definition window.\n      int baseHorizontalAddress = pageRegion.horizontalAddress\n          + displayDefinition.horizontalPositionMinimum;\n      int baseVerticalAddress = pageRegion.verticalAddress\n          + displayDefinition.verticalPositionMinimum;\n      int clipRight = Math.min(baseHorizontalAddress + regionComposition.width,\n          displayDefinition.horizontalPositionMaximum);\n      int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,\n          displayDefinition.verticalPositionMaximum);\n      canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom);\n      ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);\n      if (clutDefinition == null) {\n        clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);\n        if (clutDefinition == null) {\n          clutDefinition = defaultClutDefinition;\n        }\n      }\n\n      SparseArray<RegionObject> regionObjects = regionComposition.regionObjects;\n      for (int j = 0; j < regionObjects.size(); j++) {\n        int objectId = regionObjects.keyAt(j);\n        RegionObject regionObject = regionObjects.valueAt(j);\n        ObjectData objectData = subtitleService.objects.get(objectId);\n        if (objectData == null) {\n          objectData = subtitleService.ancillaryObjects.get(objectId);\n        }\n        if (objectData != null) {\n          @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint;\n          paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth,\n              baseHorizontalAddress + regionObject.horizontalPosition,\n              baseVerticalAddress + regionObject.verticalPosition, paint, canvas);\n        }\n      }\n\n      if (regionComposition.fillFlag) {\n        int color;\n        if (regionComposition.depth == REGION_DEPTH_8_BIT) {\n          color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit];\n        } else if (regionComposition.depth == REGION_DEPTH_4_BIT) {\n          color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit];\n        } else {\n          color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit];\n        }\n        fillRegionPaint.setColor(color);\n        canvas.drawRect(baseHorizontalAddress, baseVerticalAddress,\n            baseHorizontalAddress + regionComposition.width,\n            baseVerticalAddress + regionComposition.height,\n            fillRegionPaint);\n      }\n\n      Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress,\n          regionComposition.width, regionComposition.height);\n      cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width,\n          Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height,\n          Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width,\n          (float) regionComposition.height / displayDefinition.height));\n\n      canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);\n      // Restore clean clipping state.\n      canvas.restore();\n    }\n\n    return Collections.unmodifiableList(cues);\n  }\n\n  // Static parsing.\n\n  /**\n   * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2\n   * <p>\n   * The {@link SubtitleService} is updated with the parsed segment data.\n   */\n  private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) {\n    int segmentType = data.readBits(8);\n    int pageId = data.readBits(16);\n    int dataFieldLength = data.readBits(16);\n    int dataFieldLimit = data.getBytePosition() + dataFieldLength;\n\n    if ((dataFieldLength * 8) > data.bitsLeft()) {\n      Log.w(TAG, \"Data field length exceeds limit\");\n      // Skip to the very end.\n      data.skipBits(data.bitsLeft());\n      return;\n    }\n\n    switch (segmentType) {\n      case SEGMENT_TYPE_DISPLAY_DEFINITION:\n        if (pageId == service.subtitlePageId) {\n          service.displayDefinition = parseDisplayDefinition(data);\n        }\n        break;\n      case SEGMENT_TYPE_PAGE_COMPOSITION:\n        if (pageId == service.subtitlePageId) {\n          @Nullable PageComposition current = service.pageComposition;\n          PageComposition pageComposition = parsePageComposition(data, dataFieldLength);\n          if (pageComposition.state != PAGE_STATE_NORMAL) {\n            service.pageComposition = pageComposition;\n            service.regions.clear();\n            service.cluts.clear();\n            service.objects.clear();\n          } else if (current != null && current.version != pageComposition.version) {\n            service.pageComposition = pageComposition;\n          }\n        }\n        break;\n      case SEGMENT_TYPE_REGION_COMPOSITION:\n        @Nullable PageComposition pageComposition = service.pageComposition;\n        if (pageId == service.subtitlePageId && pageComposition != null) {\n          RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength);\n          if (pageComposition.state == PAGE_STATE_NORMAL) {\n            @Nullable\n            RegionComposition existingRegionComposition = service.regions.get(regionComposition.id);\n            if (existingRegionComposition != null) {\n              regionComposition.mergeFrom(existingRegionComposition);\n            }\n          }\n          service.regions.put(regionComposition.id, regionComposition);\n        }\n        break;\n      case SEGMENT_TYPE_CLUT_DEFINITION:\n        if (pageId == service.subtitlePageId) {\n          ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);\n          service.cluts.put(clutDefinition.id, clutDefinition);\n        } else if (pageId == service.ancillaryPageId) {\n          ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);\n          service.ancillaryCluts.put(clutDefinition.id, clutDefinition);\n        }\n        break;\n      case SEGMENT_TYPE_OBJECT_DATA:\n        if (pageId == service.subtitlePageId) {\n          ObjectData objectData = parseObjectData(data);\n          service.objects.put(objectData.id, objectData);\n        } else if (pageId == service.ancillaryPageId) {\n          ObjectData objectData = parseObjectData(data);\n          service.ancillaryObjects.put(objectData.id, objectData);\n        }\n        break;\n      default:\n        // Do nothing.\n        break;\n    }\n\n    // Skip to the next segment.\n    data.skipBytes(dataFieldLimit - data.getBytePosition());\n  }\n\n  /**\n   * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1.\n   */\n  private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) {\n    data.skipBits(4); // dds_version_number (4).\n    boolean displayWindowFlag = data.readBit();\n    data.skipBits(3); // Skip reserved.\n    int width = data.readBits(16);\n    int height = data.readBits(16);\n\n    int horizontalPositionMinimum;\n    int horizontalPositionMaximum;\n    int verticalPositionMinimum;\n    int verticalPositionMaximum;\n    if (displayWindowFlag) {\n      horizontalPositionMinimum = data.readBits(16);\n      horizontalPositionMaximum = data.readBits(16);\n      verticalPositionMinimum = data.readBits(16);\n      verticalPositionMaximum = data.readBits(16);\n    } else {\n      horizontalPositionMinimum = 0;\n      horizontalPositionMaximum = width;\n      verticalPositionMinimum = 0;\n      verticalPositionMaximum = height;\n    }\n\n    return new DisplayDefinition(width, height, horizontalPositionMinimum,\n        horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum);\n  }\n\n  /**\n   * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2.\n   */\n  private static PageComposition parsePageComposition(ParsableBitArray data, int length) {\n    int timeoutSecs = data.readBits(8);\n    int version = data.readBits(4);\n    int state = data.readBits(2);\n    data.skipBits(2);\n    int remainingLength = length - 2;\n\n    SparseArray<PageRegion> regions = new SparseArray<>();\n    while (remainingLength > 0) {\n      int regionId = data.readBits(8);\n      data.skipBits(8); // Skip reserved.\n      int regionHorizontalAddress = data.readBits(16);\n      int regionVerticalAddress = data.readBits(16);\n      remainingLength -= 6;\n      regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress));\n    }\n\n    return new PageComposition(timeoutSecs, version, state, regions);\n  }\n\n  /**\n   * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3.\n   */\n  private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) {\n    int id = data.readBits(8);\n    data.skipBits(4); // Skip region_version_number\n    boolean fillFlag = data.readBit();\n    data.skipBits(3); // Skip reserved.\n    int width = data.readBits(16);\n    int height = data.readBits(16);\n    int levelOfCompatibility = data.readBits(3);\n    int depth = data.readBits(3);\n    data.skipBits(2); // Skip reserved.\n    int clutId = data.readBits(8);\n    int pixelCode8Bit = data.readBits(8);\n    int pixelCode4Bit = data.readBits(4);\n    int pixelCode2Bit = data.readBits(2);\n    data.skipBits(2); // Skip reserved\n    int remainingLength = length - 10;\n\n    SparseArray<RegionObject> regionObjects = new SparseArray<>();\n    while (remainingLength > 0) {\n      int objectId = data.readBits(16);\n      int objectType = data.readBits(2);\n      int objectProvider = data.readBits(2);\n      int objectHorizontalPosition = data.readBits(12);\n      data.skipBits(4); // Skip reserved.\n      int objectVerticalPosition = data.readBits(12);\n      remainingLength -= 6;\n\n      int foregroundPixelCode = 0;\n      int backgroundPixelCode = 0;\n      if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles.\n        foregroundPixelCode = data.readBits(8);\n        backgroundPixelCode = data.readBits(8);\n        remainingLength -= 2;\n      }\n\n      regionObjects.put(objectId, new RegionObject(objectType, objectProvider,\n          objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode,\n          backgroundPixelCode));\n    }\n\n    return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId,\n        pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects);\n  }\n\n  /**\n   * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4.\n   */\n  private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) {\n    int clutId = data.readBits(8);\n    data.skipBits(8); // Skip clut_version_number (4), reserved (4)\n    int remainingLength = length - 2;\n\n    int[] clutEntries2Bit = generateDefault2BitClutEntries();\n    int[] clutEntries4Bit = generateDefault4BitClutEntries();\n    int[] clutEntries8Bit = generateDefault8BitClutEntries();\n\n    while (remainingLength > 0) {\n      int entryId = data.readBits(8);\n      int entryFlags = data.readBits(8);\n      remainingLength -= 2;\n\n      int[] clutEntries;\n      if ((entryFlags & 0x80) != 0) {\n        clutEntries = clutEntries2Bit;\n      } else if ((entryFlags & 0x40) != 0) {\n        clutEntries = clutEntries4Bit;\n      } else {\n        clutEntries = clutEntries8Bit;\n      }\n\n      int y;\n      int cr;\n      int cb;\n      int t;\n      if ((entryFlags & 0x01) != 0) {\n        y = data.readBits(8);\n        cr = data.readBits(8);\n        cb = data.readBits(8);\n        t = data.readBits(8);\n        remainingLength -= 4;\n      } else {\n        y = data.readBits(6) << 2;\n        cr = data.readBits(4) << 4;\n        cb = data.readBits(4) << 4;\n        t = data.readBits(2) << 6;\n        remainingLength -= 2;\n      }\n\n      if (y == 0x00) {\n        cr = 0x00;\n        cb = 0x00;\n        t = 0xFF;\n      }\n\n      int a = (byte) (0xFF - (t & 0xFF));\n      int r = (int) (y + (1.40200 * (cr - 128)));\n      int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));\n      int b = (int) (y + (1.77200 * (cb - 128)));\n      clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255),\n          Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255));\n    }\n\n    return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit);\n  }\n\n  /**\n   * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5.\n   *\n   * @return The parsed object data.\n   */\n  private static ObjectData parseObjectData(ParsableBitArray data) {\n    int objectId = data.readBits(16);\n    data.skipBits(4); // Skip object_version_number\n    int objectCodingMethod = data.readBits(2);\n    boolean nonModifyingColorFlag = data.readBit();\n    data.skipBits(1); // Skip reserved.\n\n    @Nullable byte[] topFieldData = null;\n    @Nullable byte[] bottomFieldData = null;\n\n    if (objectCodingMethod == OBJECT_CODING_STRING) {\n      int numberOfCodes = data.readBits(8);\n      // TODO: Parse and use character_codes.\n      data.skipBits(numberOfCodes * 16); // Skip character_codes.\n    } else if (objectCodingMethod == OBJECT_CODING_PIXELS) {\n      int topFieldDataLength = data.readBits(16);\n      int bottomFieldDataLength = data.readBits(16);\n      if (topFieldDataLength > 0) {\n        topFieldData = new byte[topFieldDataLength];\n        data.readBytes(topFieldData, 0, topFieldDataLength);\n      }\n      if (bottomFieldDataLength > 0) {\n        bottomFieldData = new byte[bottomFieldDataLength];\n        data.readBytes(bottomFieldData, 0, bottomFieldDataLength);\n      } else {\n        bottomFieldData = topFieldData;\n      }\n    }\n\n    return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData);\n  }\n\n  private static int[] generateDefault2BitClutEntries() {\n    int[] entries = new int[4];\n    entries[0] = 0x00000000;\n    entries[1] = 0xFFFFFFFF;\n    entries[2] = 0xFF000000;\n    entries[3] = 0xFF7F7F7F;\n    return entries;\n  }\n\n  private static int[] generateDefault4BitClutEntries() {\n    int[] entries = new int[16];\n    entries[0] = 0x00000000;\n    for (int i = 1; i < entries.length; i++) {\n      if (i < 8) {\n        entries[i] = getColor(\n            0xFF,\n            ((i & 0x01) != 0 ? 0xFF : 0x00),\n            ((i & 0x02) != 0 ? 0xFF : 0x00),\n            ((i & 0x04) != 0 ? 0xFF : 0x00));\n      } else {\n        entries[i] = getColor(\n            0xFF,\n            ((i & 0x01) != 0 ? 0x7F : 0x00),\n            ((i & 0x02) != 0 ? 0x7F : 0x00),\n            ((i & 0x04) != 0 ? 0x7F : 0x00));\n      }\n    }\n    return entries;\n  }\n\n  private static int[] generateDefault8BitClutEntries() {\n    int[] entries = new int[256];\n    entries[0] = 0x00000000;\n    for (int i = 0; i < entries.length; i++) {\n      if (i < 8) {\n        entries[i] = getColor(\n            0x3F,\n            ((i & 0x01) != 0 ? 0xFF : 0x00),\n            ((i & 0x02) != 0 ? 0xFF : 0x00),\n            ((i & 0x04) != 0 ? 0xFF : 0x00));\n      } else {\n        switch (i & 0x88) {\n          case 0x00:\n            entries[i] = getColor(\n                0xFF,\n                (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),\n                (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),\n                (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));\n            break;\n          case 0x08:\n            entries[i] = getColor(\n                0x7F,\n                (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),\n                (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),\n                (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));\n            break;\n          case 0x80:\n            entries[i] = getColor(\n                0xFF,\n                (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),\n                (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),\n                (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));\n            break;\n          case 0x88:\n            entries[i] = getColor(\n                0xFF,\n                (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),\n                (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),\n                (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));\n            break;\n        }\n      }\n    }\n    return entries;\n  }\n\n  private static int getColor(int a, int r, int g, int b) {\n    return (a << 24) | (r << 16) | (g << 8) | b;\n  }\n\n  // Static drawing.\n\n  /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */\n  private static void paintPixelDataSubBlocks(\n      ObjectData objectData,\n      ClutDefinition clutDefinition,\n      int regionDepth,\n      int horizontalAddress,\n      int verticalAddress,\n      @Nullable Paint paint,\n      Canvas canvas) {\n    int[] clutEntries;\n    if (regionDepth == REGION_DEPTH_8_BIT) {\n      clutEntries = clutDefinition.clutEntries8Bit;\n    } else if (regionDepth == REGION_DEPTH_4_BIT) {\n      clutEntries = clutDefinition.clutEntries4Bit;\n    } else {\n      clutEntries = clutDefinition.clutEntries2Bit;\n    }\n    paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress,\n        verticalAddress, paint, canvas);\n    paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress,\n        verticalAddress + 1, paint, canvas);\n  }\n\n  /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */\n  private static void paintPixelDataSubBlock(\n      byte[] pixelData,\n      int[] clutEntries,\n      int regionDepth,\n      int horizontalAddress,\n      int verticalAddress,\n      @Nullable Paint paint,\n      Canvas canvas) {\n    ParsableBitArray data = new ParsableBitArray(pixelData);\n    int column = horizontalAddress;\n    int line = verticalAddress;\n    @Nullable byte[] clutMapTable2To4 = null;\n    @Nullable byte[] clutMapTable2To8 = null;\n    @Nullable byte[] clutMapTable4To8 = null;\n\n    while (data.bitsLeft() != 0) {\n      int dataType = data.readBits(8);\n      switch (dataType) {\n        case DATA_TYPE_2BP_CODE_STRING:\n          @Nullable byte[] clutMapTable2ToX;\n          if (regionDepth == REGION_DEPTH_8_BIT) {\n            clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8;\n          } else if (regionDepth == REGION_DEPTH_4_BIT) {\n            clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4;\n          } else {\n            clutMapTable2ToX = null;\n          }\n          column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line,\n              paint, canvas);\n          data.byteAlign();\n          break;\n        case DATA_TYPE_4BP_CODE_STRING:\n          @Nullable byte[] clutMapTable4ToX;\n          if (regionDepth == REGION_DEPTH_8_BIT) {\n            clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8;\n          } else {\n            clutMapTable4ToX = null;\n          }\n          column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line,\n              paint, canvas);\n          data.byteAlign();\n          break;\n        case DATA_TYPE_8BP_CODE_STRING:\n          column =\n              paint8BitPixelCodeString(\n                  data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas);\n          break;\n        case DATA_TYPE_24_TABLE_DATA:\n          clutMapTable2To4 = buildClutMapTable(4, 4, data);\n          break;\n        case DATA_TYPE_28_TABLE_DATA:\n          clutMapTable2To8 = buildClutMapTable(4, 8, data);\n          break;\n        case DATA_TYPE_48_TABLE_DATA:\n          clutMapTable4To8 = buildClutMapTable(16, 8, data);\n          break;\n        case DATA_TYPE_END_LINE:\n          column = horizontalAddress;\n          line += 2;\n          break;\n        default:\n          // Do nothing.\n          break;\n      }\n    }\n  }\n\n  /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */\n  private static int paint2BitPixelCodeString(\n      ParsableBitArray data,\n      int[] clutEntries,\n      @Nullable byte[] clutMapTable,\n      int column,\n      int line,\n      @Nullable Paint paint,\n      Canvas canvas) {\n    boolean endOfPixelCodeString = false;\n    do {\n      int runLength = 0;\n      int clutIndex = 0;\n      int peek = data.readBits(2);\n      if (peek != 0x00) {\n        runLength = 1;\n        clutIndex = peek;\n      } else if (data.readBit()) {\n        runLength = 3 + data.readBits(3);\n        clutIndex = data.readBits(2);\n      } else if (data.readBit()) {\n        runLength = 1;\n      } else {\n        switch (data.readBits(2)) {\n          case 0x00:\n            endOfPixelCodeString = true;\n            break;\n          case 0x01:\n            runLength = 2;\n            break;\n          case 0x02:\n            runLength = 12 + data.readBits(4);\n            clutIndex = data.readBits(2);\n            break;\n          case 0x03:\n            runLength = 29 + data.readBits(8);\n            clutIndex = data.readBits(2);\n            break;\n        }\n      }\n\n      if (runLength != 0 && paint != null) {\n        paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);\n        canvas.drawRect(column, line, column + runLength, line + 1, paint);\n      }\n\n      column += runLength;\n    } while (!endOfPixelCodeString);\n\n    return column;\n  }\n\n  /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */\n  private static int paint4BitPixelCodeString(\n      ParsableBitArray data,\n      int[] clutEntries,\n      @Nullable byte[] clutMapTable,\n      int column,\n      int line,\n      @Nullable Paint paint,\n      Canvas canvas) {\n    boolean endOfPixelCodeString = false;\n    do {\n      int runLength = 0;\n      int clutIndex = 0;\n      int peek = data.readBits(4);\n      if (peek != 0x00) {\n        runLength = 1;\n        clutIndex = peek;\n      } else if (!data.readBit()) {\n        peek = data.readBits(3);\n        if (peek != 0x00) {\n          runLength = 2 + peek;\n          clutIndex = 0x00;\n        } else {\n          endOfPixelCodeString = true;\n        }\n      } else if (!data.readBit()) {\n        runLength = 4 + data.readBits(2);\n        clutIndex = data.readBits(4);\n      } else {\n        switch (data.readBits(2)) {\n          case 0x00:\n            runLength = 1;\n            break;\n          case 0x01:\n            runLength = 2;\n            break;\n          case 0x02:\n            runLength = 9 + data.readBits(4);\n            clutIndex = data.readBits(4);\n            break;\n          case 0x03:\n            runLength = 25 + data.readBits(8);\n            clutIndex = data.readBits(4);\n            break;\n        }\n      }\n\n      if (runLength != 0 && paint != null) {\n        paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);\n        canvas.drawRect(column, line, column + runLength, line + 1, paint);\n      }\n\n      column += runLength;\n    } while (!endOfPixelCodeString);\n\n    return column;\n  }\n\n  /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */\n  private static int paint8BitPixelCodeString(\n      ParsableBitArray data,\n      int[] clutEntries,\n      @Nullable byte[] clutMapTable,\n      int column,\n      int line,\n      @Nullable Paint paint,\n      Canvas canvas) {\n    boolean endOfPixelCodeString = false;\n    do {\n      int runLength = 0;\n      int clutIndex = 0;\n      int peek = data.readBits(8);\n      if (peek != 0x00) {\n        runLength = 1;\n        clutIndex = peek;\n      } else {\n        if (!data.readBit()) {\n          peek = data.readBits(7);\n          if (peek != 0x00) {\n            runLength = peek;\n            clutIndex = 0x00;\n          } else {\n            endOfPixelCodeString = true;\n          }\n        } else {\n          runLength = data.readBits(7);\n          clutIndex = data.readBits(8);\n        }\n      }\n\n      if (runLength != 0 && paint != null) {\n        paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);\n        canvas.drawRect(column, line, column + runLength, line + 1, paint);\n      }\n      column += runLength;\n    } while (!endOfPixelCodeString);\n\n    return column;\n  }\n\n  private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) {\n    byte[] clutMapTable = new byte[length];\n    for (int i = 0; i < length; i++) {\n      clutMapTable[i] = (byte) data.readBits(bitsPerEntry);\n    }\n    return clutMapTable;\n  }\n\n  // Private inner classes.\n\n  /**\n   * The subtitle service definition.\n   */\n  private static final class SubtitleService {\n\n    public final int subtitlePageId;\n    public final int ancillaryPageId;\n\n    public final SparseArray<RegionComposition> regions;\n    public final SparseArray<ClutDefinition> cluts;\n    public final SparseArray<ObjectData> objects;\n    public final SparseArray<ClutDefinition> ancillaryCluts;\n    public final SparseArray<ObjectData> ancillaryObjects;\n\n    @Nullable public DisplayDefinition displayDefinition;\n    @Nullable public PageComposition pageComposition;\n\n    public SubtitleService(int subtitlePageId, int ancillaryPageId) {\n      this.subtitlePageId = subtitlePageId;\n      this.ancillaryPageId = ancillaryPageId;\n      regions = new SparseArray<>();\n      cluts = new SparseArray<>();\n      objects = new SparseArray<>();\n      ancillaryCluts = new SparseArray<>();\n      ancillaryObjects = new SparseArray<>();\n    }\n\n    public void reset() {\n      regions.clear();\n      cluts.clear();\n      objects.clear();\n      ancillaryCluts.clear();\n      ancillaryObjects.clear();\n      displayDefinition = null;\n      pageComposition = null;\n    }\n\n  }\n\n  /**\n   * Contains the geometry and active area of the subtitle service.\n   * <p>\n   * See ETSI EN 300 743 7.2.1\n   */\n  private static final class DisplayDefinition {\n\n    public final int width;\n    public final int height;\n\n    public final int horizontalPositionMinimum;\n    public final int horizontalPositionMaximum;\n    public final int verticalPositionMinimum;\n    public final int verticalPositionMaximum;\n\n    public DisplayDefinition(int width, int height, int horizontalPositionMinimum,\n        int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) {\n      this.width = width;\n      this.height = height;\n      this.horizontalPositionMinimum = horizontalPositionMinimum;\n      this.horizontalPositionMaximum = horizontalPositionMaximum;\n      this.verticalPositionMinimum = verticalPositionMinimum;\n      this.verticalPositionMaximum = verticalPositionMaximum;\n    }\n\n  }\n\n  /**\n   *  The page is the definition and arrangement of regions in the screen.\n   *  <p>\n   *  See ETSI EN 300 743 7.2.2\n   */\n  private static final class PageComposition {\n\n    public final int timeOutSecs; // TODO: Use this or remove it.\n    public final int version;\n    public final int state;\n    public final SparseArray<PageRegion> regions;\n\n    public PageComposition(int timeoutSecs, int version, int state,\n        SparseArray<PageRegion> regions) {\n      this.timeOutSecs = timeoutSecs;\n      this.version = version;\n      this.state = state;\n      this.regions = regions;\n    }\n\n  }\n\n  /**\n   * A region within a {@link PageComposition}.\n   * <p>\n   * See ETSI EN 300 743 7.2.2\n   */\n  private static final class PageRegion {\n\n    public final int horizontalAddress;\n    public final int verticalAddress;\n\n    public PageRegion(int horizontalAddress, int verticalAddress) {\n      this.horizontalAddress = horizontalAddress;\n      this.verticalAddress = verticalAddress;\n    }\n\n  }\n\n  /**\n   * An area of the page composed of a list of objects and a CLUT.\n   * <p>\n   * See ETSI EN 300 743 7.2.3\n   */\n  private static final class RegionComposition {\n\n    public final int id;\n    public final boolean fillFlag;\n    public final int width;\n    public final int height;\n    public final int levelOfCompatibility; // TODO: Use this or remove it.\n    public final int depth;\n    public final int clutId;\n    public final int pixelCode8Bit;\n    public final int pixelCode4Bit;\n    public final int pixelCode2Bit;\n    public final SparseArray<RegionObject> regionObjects;\n\n    public RegionComposition(int id, boolean fillFlag, int width, int height,\n        int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit,\n        int pixelCode2Bit, SparseArray<RegionObject> regionObjects) {\n      this.id = id;\n      this.fillFlag = fillFlag;\n      this.width = width;\n      this.height = height;\n      this.levelOfCompatibility = levelOfCompatibility;\n      this.depth = depth;\n      this.clutId = clutId;\n      this.pixelCode8Bit = pixelCode8Bit;\n      this.pixelCode4Bit = pixelCode4Bit;\n      this.pixelCode2Bit = pixelCode2Bit;\n      this.regionObjects = regionObjects;\n    }\n\n    public void mergeFrom(RegionComposition otherRegionComposition) {\n      SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects;\n      for (int i = 0; i < otherRegionObjects.size(); i++) {\n        regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i));\n      }\n    }\n\n  }\n\n  /**\n   * An object within a {@link RegionComposition}.\n   * <p>\n   * See ETSI EN 300 743 7.2.3\n   */\n  private static final class RegionObject {\n\n    public final int type; // TODO: Use this or remove it.\n    public final int provider; // TODO: Use this or remove it.\n    public final int horizontalPosition;\n    public final int verticalPosition;\n    public final int foregroundPixelCode; // TODO: Use this or remove it.\n    public final int backgroundPixelCode; // TODO: Use this or remove it.\n\n    public RegionObject(int type, int provider, int horizontalPosition,\n        int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) {\n      this.type = type;\n      this.provider = provider;\n      this.horizontalPosition = horizontalPosition;\n      this.verticalPosition = verticalPosition;\n      this.foregroundPixelCode = foregroundPixelCode;\n      this.backgroundPixelCode = backgroundPixelCode;\n    }\n\n  }\n\n  /**\n   * CLUT family definition containing the color tables for the three bit depths defined\n   * <p>\n   * See ETSI EN 300 743 7.2.4\n   */\n  private static final class ClutDefinition {\n\n    public final int id;\n    public final int[] clutEntries2Bit;\n    public final int[] clutEntries4Bit;\n    public final int[] clutEntries8Bit;\n\n    public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit,\n        int[] clutEntries8bit) {\n      this.id = id;\n      this.clutEntries2Bit = clutEntries2Bit;\n      this.clutEntries4Bit = clutEntries4Bit;\n      this.clutEntries8Bit = clutEntries8bit;\n    }\n\n  }\n\n  /**\n   * The textual or graphical representation of an object.\n   * <p>\n   * See ETSI EN 300 743 7.2.5\n   */\n  private static final class ObjectData {\n\n    public final int id;\n    public final boolean nonModifyingColorFlag;\n    public final byte[] topFieldData;\n    public final byte[] bottomFieldData;\n\n    public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData,\n        byte[] bottomFieldData) {\n      this.id = id;\n      this.nonModifyingColorFlag = nonModifyingColorFlag;\n      this.topFieldData = topFieldData;\n      this.bottomFieldData = bottomFieldData;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.dvb;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport java.util.List;\n\n/**\n * A representation of a DVB subtitle.\n */\n/* package */ final class DvbSubtitle implements Subtitle {\n\n  private final List<Cue> cues;\n\n  public DvbSubtitle(List<Cue> cues) {\n    this.cues = cues;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return 1;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    return 0;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return cues;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.dvb;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.pgs;\n\nimport android.graphics.Bitmap;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.zip.Inflater;\n\n/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */\npublic final class PgsDecoder extends SimpleSubtitleDecoder {\n\n  private static final int SECTION_TYPE_PALETTE = 0x14;\n  private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15;\n  private static final int SECTION_TYPE_IDENTIFIER = 0x16;\n  private static final int SECTION_TYPE_END = 0x80;\n\n  private static final byte INFLATE_HEADER = 0x78;\n\n  private final ParsableByteArray buffer;\n  private final ParsableByteArray inflatedBuffer;\n  private final CueBuilder cueBuilder;\n\n  @Nullable private Inflater inflater;\n\n  public PgsDecoder() {\n    super(\"PgsDecoder\");\n    buffer = new ParsableByteArray();\n    inflatedBuffer = new ParsableByteArray();\n    cueBuilder = new CueBuilder();\n  }\n\n  @Override\n  protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException {\n    buffer.reset(data, size);\n    maybeInflateData(buffer);\n    cueBuilder.reset();\n    ArrayList<Cue> cues = new ArrayList<>();\n    while (buffer.bytesLeft() >= 3) {\n      Cue cue = readNextSection(buffer, cueBuilder);\n      if (cue != null) {\n        cues.add(cue);\n      }\n    }\n    return new PgsSubtitle(Collections.unmodifiableList(cues));\n  }\n\n  private void maybeInflateData(ParsableByteArray buffer) {\n    if (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == INFLATE_HEADER) {\n      if (inflater == null) {\n        inflater = new Inflater();\n      }\n      if (Util.inflate(buffer, inflatedBuffer, inflater)) {\n        buffer.reset(inflatedBuffer.data, inflatedBuffer.limit());\n      } // else assume data is not compressed.\n    }\n  }\n\n  @Nullable\n  private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) {\n    int limit = buffer.limit();\n    int sectionType = buffer.readUnsignedByte();\n    int sectionLength = buffer.readUnsignedShort();\n\n    int nextSectionPosition = buffer.getPosition() + sectionLength;\n    if (nextSectionPosition > limit) {\n      buffer.setPosition(limit);\n      return null;\n    }\n\n    Cue cue = null;\n    switch (sectionType) {\n      case SECTION_TYPE_PALETTE:\n        cueBuilder.parsePaletteSection(buffer, sectionLength);\n        break;\n      case SECTION_TYPE_BITMAP_PICTURE:\n        cueBuilder.parseBitmapSection(buffer, sectionLength);\n        break;\n      case SECTION_TYPE_IDENTIFIER:\n        cueBuilder.parseIdentifierSection(buffer, sectionLength);\n        break;\n      case SECTION_TYPE_END:\n        cue = cueBuilder.build();\n        cueBuilder.reset();\n        break;\n      default:\n        break;\n    }\n\n    buffer.setPosition(nextSectionPosition);\n    return cue;\n  }\n\n  private static final class CueBuilder {\n\n    private final ParsableByteArray bitmapData;\n    private final int[] colors;\n\n    private boolean colorsSet;\n    private int planeWidth;\n    private int planeHeight;\n    private int bitmapX;\n    private int bitmapY;\n    private int bitmapWidth;\n    private int bitmapHeight;\n\n    public CueBuilder() {\n      bitmapData = new ParsableByteArray();\n      colors = new int[256];\n    }\n\n    private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) {\n      if ((sectionLength % 5) != 2) {\n        // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries.\n        return;\n      }\n      buffer.skipBytes(2);\n\n      Arrays.fill(colors, 0);\n      int entryCount = sectionLength / 5;\n      for (int i = 0; i < entryCount; i++) {\n        int index = buffer.readUnsignedByte();\n        int y = buffer.readUnsignedByte();\n        int cr = buffer.readUnsignedByte();\n        int cb = buffer.readUnsignedByte();\n        int a = buffer.readUnsignedByte();\n        int r = (int) (y + (1.40200 * (cr - 128)));\n        int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));\n        int b = (int) (y + (1.77200 * (cb - 128)));\n        colors[index] =\n            (a << 24)\n                | (Util.constrainValue(r, 0, 255) << 16)\n                | (Util.constrainValue(g, 0, 255) << 8)\n                | Util.constrainValue(b, 0, 255);\n      }\n      colorsSet = true;\n    }\n\n    private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) {\n      if (sectionLength < 4) {\n        return;\n      }\n      buffer.skipBytes(3); // Id (2 bytes), version (1 byte).\n      boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0;\n      sectionLength -= 4;\n\n      if (isBaseSection) {\n        if (sectionLength < 7) {\n          return;\n        }\n        int totalLength = buffer.readUnsignedInt24();\n        if (totalLength < 4) {\n          return;\n        }\n        bitmapWidth = buffer.readUnsignedShort();\n        bitmapHeight = buffer.readUnsignedShort();\n        bitmapData.reset(totalLength - 4);\n        sectionLength -= 7;\n      }\n\n      int position = bitmapData.getPosition();\n      int limit = bitmapData.limit();\n      if (position < limit && sectionLength > 0) {\n        int bytesToRead = Math.min(sectionLength, limit - position);\n        buffer.readBytes(bitmapData.data, position, bytesToRead);\n        bitmapData.setPosition(position + bytesToRead);\n      }\n    }\n\n    private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) {\n      if (sectionLength < 19) {\n        return;\n      }\n      planeWidth = buffer.readUnsignedShort();\n      planeHeight = buffer.readUnsignedShort();\n      buffer.skipBytes(11);\n      bitmapX = buffer.readUnsignedShort();\n      bitmapY = buffer.readUnsignedShort();\n    }\n\n    @Nullable\n    public Cue build() {\n      if (planeWidth == 0\n          || planeHeight == 0\n          || bitmapWidth == 0\n          || bitmapHeight == 0\n          || bitmapData.limit() == 0\n          || bitmapData.getPosition() != bitmapData.limit()\n          || !colorsSet) {\n        return null;\n      }\n      // Build the bitmapData.\n      bitmapData.setPosition(0);\n      int[] argbBitmapData = new int[bitmapWidth * bitmapHeight];\n      int argbBitmapDataIndex = 0;\n      while (argbBitmapDataIndex < argbBitmapData.length) {\n        int colorIndex = bitmapData.readUnsignedByte();\n        if (colorIndex != 0) {\n          argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex];\n        } else {\n          int switchBits = bitmapData.readUnsignedByte();\n          if (switchBits != 0) {\n            int runLength =\n                (switchBits & 0x40) == 0\n                    ? (switchBits & 0x3F)\n                    : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte());\n            int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()];\n            Arrays.fill(\n                argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color);\n            argbBitmapDataIndex += runLength;\n          }\n        }\n      }\n      Bitmap bitmap =\n          Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);\n      // Build the cue.\n      return new Cue(\n          bitmap,\n          (float) bitmapX / planeWidth,\n          Cue.ANCHOR_TYPE_START,\n          (float) bitmapY / planeHeight,\n          Cue.ANCHOR_TYPE_START,\n          (float) bitmapWidth / planeWidth,\n          (float) bitmapHeight / planeHeight);\n    }\n\n    public void reset() {\n      planeWidth = 0;\n      planeHeight = 0;\n      bitmapX = 0;\n      bitmapY = 0;\n      bitmapWidth = 0;\n      bitmapHeight = 0;\n      bitmapData.reset(0);\n      colorsSet = false;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.pgs;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport java.util.List;\n\n/** A representation of a PGS subtitle. */\n/* package */ final class PgsSubtitle implements Subtitle {\n\n  private final List<Cue> cues;\n\n  public PgsSubtitle(List<Cue> cues) {\n    this.cues = cues;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return 1;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    return 0;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return cues;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.pgs;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ssa;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.text.Layout;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */\npublic final class SsaDecoder extends SimpleSubtitleDecoder {\n\n  private static final String TAG = \"SsaDecoder\";\n\n  private static final Pattern SSA_TIMECODE_PATTERN =\n      Pattern.compile(\"(?:(\\\\d+):)?(\\\\d+):(\\\\d+)[:.](\\\\d+)\");\n\n  /* package */ static final String FORMAT_LINE_PREFIX = \"Format:\";\n  /* package */ static final String STYLE_LINE_PREFIX = \"Style:\";\n  private static final String DIALOGUE_LINE_PREFIX = \"Dialogue:\";\n\n  private static final float DEFAULT_MARGIN = 0.05f;\n\n  private final boolean haveInitializationData;\n  @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData;\n\n  private @MonotonicNonNull Map<String, SsaStyle> styles;\n\n  /**\n   * The horizontal resolution used by the subtitle author - all cue positions are relative to this.\n   *\n   * <p>Parsed from the {@code PlayResX} value in the {@code [Script Info]} section.\n   */\n  private float screenWidth;\n  /**\n   * The vertical resolution used by the subtitle author - all cue positions are relative to this.\n   *\n   * <p>Parsed from the {@code PlayResY} value in the {@code [Script Info]} section.\n   */\n  private float screenHeight;\n\n  public SsaDecoder() {\n    this(/* initializationData= */ null);\n  }\n\n  /**\n   * Constructs an SsaDecoder with optional format and header info.\n   *\n   * @param initializationData Optional initialization data for the decoder. If not null or empty,\n   *     the initialization data must consist of two byte arrays. The first must contain an SSA\n   *     format line. The second must contain an SSA header that will be assumed common to all\n   *     samples. The header is everything in an SSA file before the {@code [Events]} section (i.e.\n   *     {@code [Script Info]} and optional {@code [V4+ Styles]} section.\n   */\n  public SsaDecoder(@Nullable List<byte[]> initializationData) {\n    super(\"SsaDecoder\");\n    screenWidth = Cue.DIMEN_UNSET;\n    screenHeight = Cue.DIMEN_UNSET;\n\n    if (initializationData != null && !initializationData.isEmpty()) {\n      haveInitializationData = true;\n      String formatLine = Util.fromUtf8Bytes(initializationData.get(0));\n      Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));\n      dialogueFormatFromInitializationData =\n          Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine));\n      parseHeader(new ParsableByteArray(initializationData.get(1)));\n    } else {\n      haveInitializationData = false;\n      dialogueFormatFromInitializationData = null;\n    }\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset) {\n    List<List<Cue>> cues = new ArrayList<>();\n    List<Long> cueTimesUs = new ArrayList<>();\n\n    ParsableByteArray data = new ParsableByteArray(bytes, length);\n    if (!haveInitializationData) {\n      parseHeader(data);\n    }\n    parseEventBody(data, cues, cueTimesUs);\n    return new SsaSubtitle(cues, cueTimesUs);\n  }\n\n  /**\n   * Parses the header of the subtitle.\n   *\n   * @param data A {@link ParsableByteArray} from which the header should be read.\n   */\n  private void parseHeader(ParsableByteArray data) {\n    @Nullable String currentLine;\n    while ((currentLine = data.readLine()) != null) {\n      if (\"[Script Info]\".equalsIgnoreCase(currentLine)) {\n        parseScriptInfo(data);\n      } else if (\"[V4+ Styles]\".equalsIgnoreCase(currentLine)) {\n        styles = parseStyles(data);\n      } else if (\"[V4 Styles]\".equalsIgnoreCase(currentLine)) {\n        Log.i(TAG, \"[V4 Styles] are not supported\");\n      } else if (\"[Events]\".equalsIgnoreCase(currentLine)) {\n        // We've reached the [Events] section, so the header is over.\n        return;\n      }\n    }\n  }\n\n  /**\n   * Parse the {@code [Script Info]} section.\n   *\n   * <p>When this returns, {@code data.position} will be set to the beginning of the first line that\n   * starts with {@code [} (i.e. the title of the next section).\n   *\n   * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position}\n   *     set to the beginning of of the first line after {@code [Script Info]}.\n   */\n  private void parseScriptInfo(ParsableByteArray data) {\n    @Nullable String currentLine;\n    while ((currentLine = data.readLine()) != null\n        && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) {\n      String[] infoNameAndValue = currentLine.split(\":\");\n      if (infoNameAndValue.length != 2) {\n        continue;\n      }\n      switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) {\n        case \"playresx\":\n          try {\n            screenWidth = Float.parseFloat(infoNameAndValue[1].trim());\n          } catch (NumberFormatException e) {\n            // Ignore invalid PlayResX value.\n          }\n          break;\n        case \"playresy\":\n          try {\n            screenHeight = Float.parseFloat(infoNameAndValue[1].trim());\n          } catch (NumberFormatException e) {\n            // Ignore invalid PlayResY value.\n          }\n          break;\n      }\n    }\n  }\n\n  /**\n   * Parse the {@code [V4+ Styles]} section.\n   *\n   * <p>When this returns, {@code data.position} will be set to the beginning of the first line that\n   * starts with {@code [} (i.e. the title of the next section).\n   *\n   * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing\n   *     at the beginning of of the first line after {@code [V4+ Styles]}.\n   */\n  private static Map<String, SsaStyle> parseStyles(ParsableByteArray data) {\n    Map<String, SsaStyle> styles = new LinkedHashMap<>();\n    @Nullable SsaStyle.Format formatInfo = null;\n    @Nullable String currentLine;\n    while ((currentLine = data.readLine()) != null\n        && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) {\n      if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {\n        formatInfo = SsaStyle.Format.fromFormatLine(currentLine);\n      } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) {\n        if (formatInfo == null) {\n          Log.w(TAG, \"Skipping 'Style:' line before 'Format:' line: \" + currentLine);\n          continue;\n        }\n        @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo);\n        if (style != null) {\n          styles.put(style.name, style);\n        }\n      }\n    }\n    return styles;\n  }\n\n  /**\n   * Parses the event body of the subtitle.\n   *\n   * @param data A {@link ParsableByteArray} from which the body should be read.\n   * @param cues A list to which parsed cues will be added.\n   * @param cueTimesUs A sorted list to which parsed cue timestamps will be added.\n   */\n  private void parseEventBody(ParsableByteArray data, List<List<Cue>> cues, List<Long> cueTimesUs) {\n    @Nullable\n    SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null;\n    @Nullable String currentLine;\n    while ((currentLine = data.readLine()) != null) {\n      if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {\n        format = SsaDialogueFormat.fromFormatLine(currentLine);\n      } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {\n        if (format == null) {\n          Log.w(TAG, \"Skipping dialogue line before complete format: \" + currentLine);\n          continue;\n        }\n        parseDialogueLine(currentLine, format, cues, cueTimesUs);\n      }\n    }\n  }\n\n  /**\n   * Parses a dialogue line.\n   *\n   * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}).\n   * @param format The dialogue format to use when parsing {@code dialogueLine}.\n   * @param cues A list to which parsed cues will be added.\n   * @param cueTimesUs A sorted list to which parsed cue timestamps will be added.\n   */\n  private void parseDialogueLine(\n      String dialogueLine, SsaDialogueFormat format, List<List<Cue>> cues, List<Long> cueTimesUs) {\n    Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX));\n    String[] lineValues =\n        dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(\",\", format.length);\n    if (lineValues.length != format.length) {\n      Log.w(TAG, \"Skipping dialogue line with fewer columns than format: \" + dialogueLine);\n      return;\n    }\n\n    long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]);\n    if (startTimeUs == C.TIME_UNSET) {\n      Log.w(TAG, \"Skipping invalid timing: \" + dialogueLine);\n      return;\n    }\n\n    long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]);\n    if (endTimeUs == C.TIME_UNSET) {\n      Log.w(TAG, \"Skipping invalid timing: \" + dialogueLine);\n      return;\n    }\n\n    @Nullable\n    SsaStyle style =\n        styles != null && format.styleIndex != C.INDEX_UNSET\n            ? styles.get(lineValues[format.styleIndex].trim())\n            : null;\n    String rawText = lineValues[format.textIndex];\n    SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText);\n    String text =\n        SsaStyle.Overrides.stripStyleOverrides(rawText)\n            .replaceAll(\"\\\\\\\\N\", \"\\n\")\n            .replaceAll(\"\\\\\\\\n\", \"\\n\");\n    Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight);\n\n    int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues);\n    int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues);\n    // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue.\n    for (int i = startTimeIndex; i < endTimeIndex; i++) {\n      cues.get(i).add(cue);\n    }\n  }\n\n  /**\n   * Parses an SSA timecode string.\n   *\n   * @param timeString The string to parse.\n   * @return The parsed timestamp in microseconds.\n   */\n  private static long parseTimecodeUs(String timeString) {\n    Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim());\n    if (!matcher.matches()) {\n      return C.TIME_UNSET;\n    }\n    long timestampUs =\n        Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second.\n    return timestampUs;\n  }\n\n  private static Cue createCue(\n      String text,\n      @Nullable SsaStyle style,\n      SsaStyle.Overrides styleOverrides,\n      float screenWidth,\n      float screenHeight) {\n    @SsaStyle.SsaAlignment int alignment;\n    if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {\n      alignment = styleOverrides.alignment;\n    } else if (style != null) {\n      alignment = style.alignment;\n    } else {\n      alignment = SsaStyle.SSA_ALIGNMENT_UNKNOWN;\n    }\n    @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment);\n    @Cue.AnchorType int lineAnchor = toLineAnchor(alignment);\n\n    float position;\n    float line;\n    if (styleOverrides.position != null\n        && screenHeight != Cue.DIMEN_UNSET\n        && screenWidth != Cue.DIMEN_UNSET) {\n      position = styleOverrides.position.x / screenWidth;\n      line = styleOverrides.position.y / screenHeight;\n    } else {\n      // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines.\n      position = computeDefaultLineOrPosition(positionAnchor);\n      line = computeDefaultLineOrPosition(lineAnchor);\n    }\n\n    return new Cue(\n        text,\n        toTextAlignment(alignment),\n        line,\n        Cue.LINE_TYPE_FRACTION,\n        lineAnchor,\n        position,\n        positionAnchor,\n        /* size= */ Cue.DIMEN_UNSET);\n  }\n\n  @Nullable\n  private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) {\n    switch (alignment) {\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_TOP_LEFT:\n        return Layout.Alignment.ALIGN_NORMAL;\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_TOP_CENTER:\n        return Layout.Alignment.ALIGN_CENTER;\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT:\n      case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT:\n        return Layout.Alignment.ALIGN_OPPOSITE;\n      case SsaStyle.SSA_ALIGNMENT_UNKNOWN:\n        return null;\n      default:\n        Log.w(TAG, \"Unknown alignment: \" + alignment);\n        return null;\n    }\n  }\n\n  @Cue.AnchorType\n  private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) {\n    switch (alignment) {\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT:\n        return Cue.ANCHOR_TYPE_END;\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT:\n        return Cue.ANCHOR_TYPE_MIDDLE;\n      case SsaStyle.SSA_ALIGNMENT_TOP_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_TOP_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT:\n        return Cue.ANCHOR_TYPE_START;\n      case SsaStyle.SSA_ALIGNMENT_UNKNOWN:\n        return Cue.TYPE_UNSET;\n      default:\n        Log.w(TAG, \"Unknown alignment: \" + alignment);\n        return Cue.TYPE_UNSET;\n    }\n  }\n\n  @Cue.AnchorType\n  private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) {\n    switch (alignment) {\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT:\n      case SsaStyle.SSA_ALIGNMENT_TOP_LEFT:\n        return Cue.ANCHOR_TYPE_START;\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER:\n      case SsaStyle.SSA_ALIGNMENT_TOP_CENTER:\n        return Cue.ANCHOR_TYPE_MIDDLE;\n      case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT:\n      case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT:\n      case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT:\n        return Cue.ANCHOR_TYPE_END;\n      case SsaStyle.SSA_ALIGNMENT_UNKNOWN:\n        return Cue.TYPE_UNSET;\n      default:\n        Log.w(TAG, \"Unknown alignment: \" + alignment);\n        return Cue.TYPE_UNSET;\n    }\n  }\n\n  private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) {\n    switch (anchor) {\n      case Cue.ANCHOR_TYPE_START:\n        return DEFAULT_MARGIN;\n      case Cue.ANCHOR_TYPE_MIDDLE:\n        return 0.5f;\n      case Cue.ANCHOR_TYPE_END:\n        return 1.0f - DEFAULT_MARGIN;\n      case Cue.TYPE_UNSET:\n      default:\n        return Cue.DIMEN_UNSET;\n    }\n  }\n\n  /**\n   * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and\n   * returns the index.\n   *\n   * <p>If it's inserted, we also insert a matching entry to {@code cues}.\n   */\n  private static int addCuePlacerholderByTime(\n      long timeUs, List<Long> sortedCueTimesUs, List<List<Cue>> cues) {\n    int insertionIndex = 0;\n    for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) {\n      if (sortedCueTimesUs.get(i) == timeUs) {\n        return i;\n      }\n\n      if (sortedCueTimesUs.get(i) < timeUs) {\n        insertionIndex = i + 1;\n        break;\n      }\n    }\n    sortedCueTimesUs.add(insertionIndex, timeUs);\n    // Copy over cues from left, or use an empty list if we're inserting at the beginning.\n    cues.add(\n        insertionIndex,\n        insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1)));\n    return insertionIndex;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ssa;\n\nimport static com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX;\n\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Represents a {@code Format:} line from the {@code [Events]} section\n *\n * <p>The indices are used to determine the location of particular properties in each {@code\n * Dialogue:} line.\n */\n/* package */ final class SsaDialogueFormat {\n\n  public final int startTimeIndex;\n  public final int endTimeIndex;\n  public final int styleIndex;\n  public final int textIndex;\n  public final int length;\n\n  private SsaDialogueFormat(\n      int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) {\n    this.startTimeIndex = startTimeIndex;\n    this.endTimeIndex = endTimeIndex;\n    this.styleIndex = styleIndex;\n    this.textIndex = textIndex;\n    this.length = length;\n  }\n\n  /**\n   * Parses the format info from a 'Format:' line in the [Events] section.\n   *\n   * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'.\n   */\n  @Nullable\n  public static SsaDialogueFormat fromFormatLine(String formatLine) {\n    int startTimeIndex = C.INDEX_UNSET;\n    int endTimeIndex = C.INDEX_UNSET;\n    int styleIndex = C.INDEX_UNSET;\n    int textIndex = C.INDEX_UNSET;\n    Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));\n    String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), \",\");\n    for (int i = 0; i < keys.length; i++) {\n      switch (Util.toLowerInvariant(keys[i].trim())) {\n        case \"start\":\n          startTimeIndex = i;\n          break;\n        case \"end\":\n          endTimeIndex = i;\n          break;\n        case \"style\":\n          styleIndex = i;\n          break;\n        case \"text\":\n          textIndex = i;\n          break;\n      }\n    }\n    return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET)\n        ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length)\n        : null;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ssa;\n\nimport static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;\nimport static java.lang.annotation.RetentionPolicy.SOURCE;\n\nimport android.graphics.PointF;\nimport android.text.TextUtils;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */\n/* package */ final class SsaStyle {\n\n  private static final String TAG = \"SsaStyle\";\n\n  /**\n   * The SSA/ASS alignments.\n   *\n   * <p>Allowed values:\n   *\n   * <ul>\n   *   <li>{@link #SSA_ALIGNMENT_UNKNOWN}\n   *   <li>{@link #SSA_ALIGNMENT_BOTTOM_LEFT}\n   *   <li>{@link #SSA_ALIGNMENT_BOTTOM_CENTER}\n   *   <li>{@link #SSA_ALIGNMENT_BOTTOM_RIGHT}\n   *   <li>{@link #SSA_ALIGNMENT_MIDDLE_LEFT}\n   *   <li>{@link #SSA_ALIGNMENT_MIDDLE_CENTER}\n   *   <li>{@link #SSA_ALIGNMENT_MIDDLE_RIGHT}\n   *   <li>{@link #SSA_ALIGNMENT_TOP_LEFT}\n   *   <li>{@link #SSA_ALIGNMENT_TOP_CENTER}\n   *   <li>{@link #SSA_ALIGNMENT_TOP_RIGHT}\n   * </ul>\n   */\n  @IntDef({\n    SSA_ALIGNMENT_UNKNOWN,\n    SSA_ALIGNMENT_BOTTOM_LEFT,\n    SSA_ALIGNMENT_BOTTOM_CENTER,\n    SSA_ALIGNMENT_BOTTOM_RIGHT,\n    SSA_ALIGNMENT_MIDDLE_LEFT,\n    SSA_ALIGNMENT_MIDDLE_CENTER,\n    SSA_ALIGNMENT_MIDDLE_RIGHT,\n    SSA_ALIGNMENT_TOP_LEFT,\n    SSA_ALIGNMENT_TOP_CENTER,\n    SSA_ALIGNMENT_TOP_RIGHT,\n  })\n  @Documented\n  @Retention(SOURCE)\n  public @interface SsaAlignment {}\n\n  // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad).\n  public static final int SSA_ALIGNMENT_UNKNOWN = -1;\n  public static final int SSA_ALIGNMENT_BOTTOM_LEFT = 1;\n  public static final int SSA_ALIGNMENT_BOTTOM_CENTER = 2;\n  public static final int SSA_ALIGNMENT_BOTTOM_RIGHT = 3;\n  public static final int SSA_ALIGNMENT_MIDDLE_LEFT = 4;\n  public static final int SSA_ALIGNMENT_MIDDLE_CENTER = 5;\n  public static final int SSA_ALIGNMENT_MIDDLE_RIGHT = 6;\n  public static final int SSA_ALIGNMENT_TOP_LEFT = 7;\n  public static final int SSA_ALIGNMENT_TOP_CENTER = 8;\n  public static final int SSA_ALIGNMENT_TOP_RIGHT = 9;\n\n  public final String name;\n  @SsaAlignment public final int alignment;\n\n  private SsaStyle(String name, @SsaAlignment int alignment) {\n    this.name = name;\n    this.alignment = alignment;\n  }\n\n  @Nullable\n  public static SsaStyle fromStyleLine(String styleLine, Format format) {\n    Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));\n    String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), \",\");\n    if (styleValues.length != format.length) {\n      Log.w(\n          TAG,\n          Util.formatInvariant(\n              \"Skipping malformed 'Style:' line (expected %s values, found %s): '%s'\",\n              format.length, styleValues.length, styleLine));\n      return null;\n    }\n    try {\n      return new SsaStyle(\n          styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex]));\n    } catch (RuntimeException e) {\n      Log.w(TAG, \"Skipping malformed 'Style:' line: '\" + styleLine + \"'\", e);\n      return null;\n    }\n  }\n\n  @SsaAlignment\n  private static int parseAlignment(String alignmentStr) {\n    try {\n      @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim());\n      if (isValidAlignment(alignment)) {\n        return alignment;\n      }\n    } catch (NumberFormatException e) {\n      // Swallow the exception and return UNKNOWN below.\n    }\n    Log.w(TAG, \"Ignoring unknown alignment: \" + alignmentStr);\n    return SSA_ALIGNMENT_UNKNOWN;\n  }\n\n  private static boolean isValidAlignment(@SsaAlignment int alignment) {\n    switch (alignment) {\n      case SSA_ALIGNMENT_BOTTOM_CENTER:\n      case SSA_ALIGNMENT_BOTTOM_LEFT:\n      case SSA_ALIGNMENT_BOTTOM_RIGHT:\n      case SSA_ALIGNMENT_MIDDLE_CENTER:\n      case SSA_ALIGNMENT_MIDDLE_LEFT:\n      case SSA_ALIGNMENT_MIDDLE_RIGHT:\n      case SSA_ALIGNMENT_TOP_CENTER:\n      case SSA_ALIGNMENT_TOP_LEFT:\n      case SSA_ALIGNMENT_TOP_RIGHT:\n        return true;\n      case SSA_ALIGNMENT_UNKNOWN:\n      default:\n        return false;\n    }\n  }\n\n  /**\n   * Represents a {@code Format:} line from the {@code [V4+ Styles]} section\n   *\n   * <p>The indices are used to determine the location of particular properties in each {@code\n   * Style:} line.\n   */\n  /* package */ static final class Format {\n\n    public final int nameIndex;\n    public final int alignmentIndex;\n    public final int length;\n\n    private Format(int nameIndex, int alignmentIndex, int length) {\n      this.nameIndex = nameIndex;\n      this.alignmentIndex = alignmentIndex;\n      this.length = length;\n    }\n\n    /**\n     * Parses the format info from a 'Format:' line in the [V4+ Styles] section.\n     *\n     * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'.\n     */\n    @Nullable\n    public static Format fromFormatLine(String styleFormatLine) {\n      int nameIndex = C.INDEX_UNSET;\n      int alignmentIndex = C.INDEX_UNSET;\n      String[] keys =\n          TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), \",\");\n      for (int i = 0; i < keys.length; i++) {\n        switch (Util.toLowerInvariant(keys[i].trim())) {\n          case \"name\":\n            nameIndex = i;\n            break;\n          case \"alignment\":\n            alignmentIndex = i;\n            break;\n        }\n      }\n      return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null;\n    }\n  }\n\n  /**\n   * Represents the style override information parsed from an SSA/ASS dialogue line.\n   *\n   * <p>Overrides are contained in braces embedded in the dialogue text of the cue.\n   */\n  /* package */ static final class Overrides {\n\n    private static final String TAG = \"SsaStyle.Overrides\";\n\n    /** Matches \"{foo}\" and returns \"foo\" in group 1 */\n    // Warning that \\\\} can be replaced with } is bogus [internal: b/144480183].\n    private static final Pattern BRACES_PATTERN = Pattern.compile(\"\\\\{([^}]*)\\\\}\");\n\n    private static final String PADDED_DECIMAL_PATTERN = \"\\\\s*\\\\d+(?:\\\\.\\\\d+)?\\\\s*\";\n\n    /** Matches \"\\pos(x,y)\" and returns \"x\" in group 1 and \"y\" in group 2 */\n    private static final Pattern POSITION_PATTERN =\n        Pattern.compile(Util.formatInvariant(\"\\\\\\\\pos\\\\((%1$s),(%1$s)\\\\)\", PADDED_DECIMAL_PATTERN));\n    /** Matches \"\\move(x1,y1,x2,y2[,t1,t2])\" and returns \"x2\" in group 1 and \"y2\" in group 2 */\n    private static final Pattern MOVE_PATTERN =\n        Pattern.compile(\n            Util.formatInvariant(\n                \"\\\\\\\\move\\\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\\\)\", PADDED_DECIMAL_PATTERN));\n\n    /** Matches \"\\anx\" and returns x in group 1 */\n    private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile(\"\\\\\\\\an(\\\\d+)\");\n\n    @SsaAlignment public final int alignment;\n    @Nullable public final PointF position;\n\n    private Overrides(@SsaAlignment int alignment, @Nullable PointF position) {\n      this.alignment = alignment;\n      this.position = position;\n    }\n\n    public static Overrides parseFromDialogue(String text) {\n      @SsaAlignment int alignment = SSA_ALIGNMENT_UNKNOWN;\n      PointF position = null;\n      Matcher matcher = BRACES_PATTERN.matcher(text);\n      while (matcher.find()) {\n        String braceContents = matcher.group(1);\n        try {\n          PointF parsedPosition = parsePosition(braceContents);\n          if (parsedPosition != null) {\n            position = parsedPosition;\n          }\n        } catch (RuntimeException e) {\n          // Ignore invalid \\pos() or \\move() function.\n        }\n        try {\n          @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents);\n          if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {\n            alignment = parsedAlignment;\n          }\n        } catch (RuntimeException e) {\n          // Ignore invalid \\an alignment override.\n        }\n      }\n      return new Overrides(alignment, position);\n    }\n\n    public static String stripStyleOverrides(String dialogueLine) {\n      return BRACES_PATTERN.matcher(dialogueLine).replaceAll(\"\");\n    }\n\n    /**\n     * Parses the position from a style override, returns null if no position is found.\n     *\n     * <p>The attribute is expected to be in the form {@code \\pos(x,y)} or {@code\n     * \\move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of\n     * {@code \\move()}, this returns {@code (x2, y2)} (i.e. the end position of the move).\n     *\n     * @param styleOverride The string to parse.\n     * @return The parsed position, or null if no position is found.\n     */\n    @Nullable\n    private static PointF parsePosition(String styleOverride) {\n      Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride);\n      Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride);\n      boolean hasPosition = positionMatcher.find();\n      boolean hasMove = moveMatcher.find();\n\n      String x;\n      String y;\n      if (hasPosition) {\n        if (hasMove) {\n          Log.i(\n              TAG,\n              \"Override has both \\\\pos(x,y) and \\\\move(x1,y1,x2,y2); using \\\\pos values. override='\"\n                  + styleOverride\n                  + \"'\");\n        }\n        x = positionMatcher.group(1);\n        y = positionMatcher.group(2);\n      } else if (hasMove) {\n        x = moveMatcher.group(1);\n        y = moveMatcher.group(2);\n      } else {\n        return null;\n      }\n      return new PointF(\n          Float.parseFloat(Assertions.checkNotNull(x).trim()),\n          Float.parseFloat(Assertions.checkNotNull(y).trim()));\n    }\n\n    @SsaAlignment\n    private static int parseAlignmentOverride(String braceContents) {\n      Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents);\n      return matcher.find() ? parseAlignment(matcher.group(1)) : SSA_ALIGNMENT_UNKNOWN;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ssa;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A representation of an SSA/ASS subtitle.\n */\n/* package */ final class SsaSubtitle implements Subtitle {\n\n  private final List<List<Cue>> cues;\n  private final List<Long> cueTimesUs;\n\n  /**\n   * @param cues The cues in the subtitle.\n   * @param cueTimesUs The cue times, in microseconds.\n   */\n  public SsaSubtitle(List<List<Cue>> cues, List<Long> cueTimesUs) {\n    this.cues = cues;\n    this.cueTimesUs = cueTimesUs;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);\n    return index < cueTimesUs.size() ? index : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return cueTimesUs.size();\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index >= 0);\n    Assertions.checkArgument(index < cueTimesUs.size());\n    return cueTimesUs.get(index);\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);\n    if (index == -1) {\n      // timeUs is earlier than the start of the first cue.\n      return Collections.emptyList();\n    } else {\n      return cues.get(index);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.ssa;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.subrip;\n\nimport android.text.Html;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.LongArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * A {@link SimpleSubtitleDecoder} for SubRip.\n */\npublic final class SubripDecoder extends SimpleSubtitleDecoder {\n\n  // Fractional positions for use when alignment tags are present.\n  private static final float START_FRACTION = 0.08f;\n  private static final float END_FRACTION = 1 - START_FRACTION;\n  private static final float MID_FRACTION = 0.5f;\n\n  private static final String TAG = \"SubripDecoder\";\n\n  private static final String SUBRIP_TIMECODE = \"(?:(\\\\d+):)?(\\\\d+):(\\\\d+),(\\\\d+)\";\n  private static final Pattern SUBRIP_TIMING_LINE =\n      Pattern.compile(\"\\\\s*(\" + SUBRIP_TIMECODE + \")\\\\s*-->\\\\s*(\" + SUBRIP_TIMECODE + \")\\\\s*\");\n\n  private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile(\"\\\\{\\\\\\\\.*?\\\\}\");\n  private static final String SUBRIP_ALIGNMENT_TAG = \"\\\\{\\\\\\\\an[1-9]\\\\}\";\n\n  // Alignment tags for SSA V4+.\n  private static final String ALIGN_BOTTOM_LEFT = \"{\\\\an1}\";\n  private static final String ALIGN_BOTTOM_MID = \"{\\\\an2}\";\n  private static final String ALIGN_BOTTOM_RIGHT = \"{\\\\an3}\";\n  private static final String ALIGN_MID_LEFT = \"{\\\\an4}\";\n  private static final String ALIGN_MID_MID = \"{\\\\an5}\";\n  private static final String ALIGN_MID_RIGHT = \"{\\\\an6}\";\n  private static final String ALIGN_TOP_LEFT = \"{\\\\an7}\";\n  private static final String ALIGN_TOP_MID = \"{\\\\an8}\";\n  private static final String ALIGN_TOP_RIGHT = \"{\\\\an9}\";\n\n  private final StringBuilder textBuilder;\n  private final ArrayList<String> tags;\n\n  public SubripDecoder() {\n    super(\"SubripDecoder\");\n    textBuilder = new StringBuilder();\n    tags = new ArrayList<>();\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset) {\n    ArrayList<Cue> cues = new ArrayList<>();\n    LongArray cueTimesUs = new LongArray();\n    ParsableByteArray subripData = new ParsableByteArray(bytes, length);\n\n    @Nullable String currentLine;\n    while ((currentLine = subripData.readLine()) != null) {\n      if (currentLine.length() == 0) {\n        // Skip blank lines.\n        continue;\n      }\n\n      // Parse the index line as a sanity check.\n      try {\n        Integer.parseInt(currentLine);\n      } catch (NumberFormatException e) {\n        Log.w(TAG, \"Skipping invalid index: \" + currentLine);\n        continue;\n      }\n\n      // Read and parse the timing line.\n      currentLine = subripData.readLine();\n      if (currentLine == null) {\n        Log.w(TAG, \"Unexpected end\");\n        break;\n      }\n\n      Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);\n      if (matcher.matches()) {\n        cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1));\n        cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));\n      } else {\n        Log.w(TAG, \"Skipping invalid timing: \" + currentLine);\n        continue;\n      }\n\n      // Read and parse the text and tags.\n      textBuilder.setLength(0);\n      tags.clear();\n      currentLine = subripData.readLine();\n      while (!TextUtils.isEmpty(currentLine)) {\n        if (textBuilder.length() > 0) {\n          textBuilder.append(\"<br>\");\n        }\n        textBuilder.append(processLine(currentLine, tags));\n        currentLine = subripData.readLine();\n      }\n\n      Spanned text = Html.fromHtml(textBuilder.toString());\n\n      @Nullable String alignmentTag = null;\n      for (int i = 0; i < tags.size(); i++) {\n        String tag = tags.get(i);\n        if (tag.matches(SUBRIP_ALIGNMENT_TAG)) {\n          alignmentTag = tag;\n          // Subsequent alignment tags should be ignored.\n          break;\n        }\n      }\n      cues.add(buildCue(text, alignmentTag));\n      cues.add(Cue.EMPTY);\n    }\n\n    Cue[] cuesArray = new Cue[cues.size()];\n    cues.toArray(cuesArray);\n    long[] cueTimesUsArray = cueTimesUs.toArray();\n    return new SubripSubtitle(cuesArray, cueTimesUsArray);\n  }\n\n  /**\n   * Trims and removes tags from the given line. The removed tags are added to {@code tags}.\n   *\n   * @param line The line to process.\n   * @param tags A list to which removed tags will be added.\n   * @return The processed line.\n   */\n  private String processLine(String line, ArrayList<String> tags) {\n    line = line.trim();\n\n    int removedCharacterCount = 0;\n    StringBuilder processedLine = new StringBuilder(line);\n    Matcher matcher = SUBRIP_TAG_PATTERN.matcher(line);\n    while (matcher.find()) {\n      String tag = matcher.group();\n      tags.add(tag);\n      int start = matcher.start() - removedCharacterCount;\n      int tagLength = tag.length();\n      processedLine.replace(start, /* end= */ start + tagLength, /* str= */ \"\");\n      removedCharacterCount += tagLength;\n    }\n\n    return processedLine.toString();\n  }\n\n  /**\n   * Build a {@link Cue} based on the given text and alignment tag.\n   *\n   * @param text The text.\n   * @param alignmentTag The alignment tag, or {@code null} if no alignment tag is available.\n   * @return Built cue\n   */\n  private Cue buildCue(Spanned text, @Nullable String alignmentTag) {\n    if (alignmentTag == null) {\n      return new Cue(text);\n    }\n\n    // Horizontal alignment.\n    @Cue.AnchorType int positionAnchor;\n    switch (alignmentTag) {\n      case ALIGN_BOTTOM_LEFT:\n      case ALIGN_MID_LEFT:\n      case ALIGN_TOP_LEFT:\n        positionAnchor = Cue.ANCHOR_TYPE_START;\n        break;\n      case ALIGN_BOTTOM_RIGHT:\n      case ALIGN_MID_RIGHT:\n      case ALIGN_TOP_RIGHT:\n        positionAnchor = Cue.ANCHOR_TYPE_END;\n        break;\n      case ALIGN_BOTTOM_MID:\n      case ALIGN_MID_MID:\n      case ALIGN_TOP_MID:\n      default:\n        positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;\n        break;\n    }\n\n    // Vertical alignment.\n    @Cue.AnchorType int lineAnchor;\n    switch (alignmentTag) {\n      case ALIGN_BOTTOM_LEFT:\n      case ALIGN_BOTTOM_MID:\n      case ALIGN_BOTTOM_RIGHT:\n        lineAnchor = Cue.ANCHOR_TYPE_END;\n        break;\n      case ALIGN_TOP_LEFT:\n      case ALIGN_TOP_MID:\n      case ALIGN_TOP_RIGHT:\n        lineAnchor = Cue.ANCHOR_TYPE_START;\n        break;\n      case ALIGN_MID_LEFT:\n      case ALIGN_MID_MID:\n      case ALIGN_MID_RIGHT:\n      default:\n        lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;\n        break;\n    }\n\n    return new Cue(\n        text,\n        /* textAlignment= */ null,\n        getFractionalPositionForAnchorType(lineAnchor),\n        Cue.LINE_TYPE_FRACTION,\n        lineAnchor,\n        getFractionalPositionForAnchorType(positionAnchor),\n        positionAnchor,\n        Cue.DIMEN_UNSET);\n  }\n\n  private static long parseTimecode(Matcher matcher, int groupOffset) {\n    long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000;\n    timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000;\n    timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000;\n    timestampMs += Long.parseLong(matcher.group(groupOffset + 4));\n    return timestampMs * 1000;\n  }\n\n  /* package */ static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {\n    switch (anchorType) {\n      case Cue.ANCHOR_TYPE_START:\n        return SubripDecoder.START_FRACTION;\n      case Cue.ANCHOR_TYPE_MIDDLE:\n        return SubripDecoder.MID_FRACTION;\n      case Cue.ANCHOR_TYPE_END:\n        return SubripDecoder.END_FRACTION;\n      case Cue.TYPE_UNSET:\n      default:\n        // Should never happen.\n        throw new IllegalArgumentException();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.subrip;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A representation of a SubRip subtitle.\n */\n/* package */ final class SubripSubtitle implements Subtitle {\n\n  private final Cue[] cues;\n  private final long[] cueTimesUs;\n\n  /**\n   * @param cues The cues in the subtitle.\n   * @param cueTimesUs The cue times, in microseconds.\n   */\n  public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {\n    this.cues = cues;\n    this.cueTimesUs = cueTimesUs;\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);\n    return index < cueTimesUs.length ? index : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return cueTimesUs.length;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index >= 0);\n    Assertions.checkArgument(index < cueTimesUs.length);\n    return cueTimesUs[index];\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);\n    if (index == -1 || cues[index] == Cue.EMPTY) {\n      // timeUs is earlier than the start of the first cue, or we have an empty cue.\n      return Collections.emptyList();\n    } else {\n      return Collections.singletonList(cues[index]);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.subrip;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport android.text.Layout;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ColorParser;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.util.XmlPullParserUtil;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.util.ArrayDeque;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\nimport org.xmlpull.v1.XmlPullParserFactory;\n\n/**\n * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features\n * supported by this decoder are:\n *\n * <ul>\n *   <li>content\n *   <li>core\n *   <li>presentation\n *   <li>profile\n *   <li>structure\n *   <li>time-offset\n *   <li>timing\n *   <li>tickRate\n *   <li>time-clock-with-frames\n *   <li>time-clock\n *   <li>time-offset-with-frames\n *   <li>time-offset-with-ticks\n *   <li>cell-resolution\n * </ul>\n *\n * @see <a href=\"http://www.w3.org/TR/ttaf1-dfxp/\">TTML specification</a>\n */\npublic final class TtmlDecoder extends SimpleSubtitleDecoder {\n\n  private static final String TAG = \"TtmlDecoder\";\n\n  private static final String TTP = \"http://www.w3.org/ns/ttml#parameter\";\n\n  private static final String ATTR_BEGIN = \"begin\";\n  private static final String ATTR_DURATION = \"dur\";\n  private static final String ATTR_END = \"end\";\n  private static final String ATTR_STYLE = \"style\";\n  private static final String ATTR_REGION = \"region\";\n  private static final String ATTR_IMAGE = \"backgroundImage\";\n\n  private static final Pattern CLOCK_TIME =\n      Pattern.compile(\"^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])\"\n          + \"(?:(\\\\.[0-9]+)|:([0-9][0-9])(?:\\\\.([0-9]+))?)?$\");\n  private static final Pattern OFFSET_TIME =\n      Pattern.compile(\"^([0-9]+(?:\\\\.[0-9]+)?)(h|m|s|ms|f|t)$\");\n  private static final Pattern FONT_SIZE = Pattern.compile(\"^(([0-9]*.)?[0-9]+)(px|em|%)$\");\n  private static final Pattern PERCENTAGE_COORDINATES =\n      Pattern.compile(\"^(\\\\d+\\\\.?\\\\d*?)% (\\\\d+\\\\.?\\\\d*?)%$\");\n  private static final Pattern PIXEL_COORDINATES =\n      Pattern.compile(\"^(\\\\d+\\\\.?\\\\d*?)px (\\\\d+\\\\.?\\\\d*?)px$\");\n  private static final Pattern CELL_RESOLUTION = Pattern.compile(\"^(\\\\d+) (\\\\d+)$\");\n\n  private static final int DEFAULT_FRAME_RATE = 30;\n\n  private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =\n      new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);\n  private static final CellResolution DEFAULT_CELL_RESOLUTION =\n      new CellResolution(/* columns= */ 32, /* rows= */ 15);\n\n  private final XmlPullParserFactory xmlParserFactory;\n\n  public TtmlDecoder() {\n    super(\"TtmlDecoder\");\n    try {\n      xmlParserFactory = XmlPullParserFactory.newInstance();\n      xmlParserFactory.setNamespaceAware(true);\n    } catch (XmlPullParserException e) {\n      throw new RuntimeException(\"Couldn't create XmlPullParserFactory instance\", e);\n    }\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset)\n      throws SubtitleDecoderException {\n    try {\n      XmlPullParser xmlParser = xmlParserFactory.newPullParser();\n      Map<String, TtmlStyle> globalStyles = new HashMap<>();\n      Map<String, TtmlRegion> regionMap = new HashMap<>();\n      Map<String, String> imageMap = new HashMap<>();\n      regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));\n      ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);\n      xmlParser.setInput(inputStream, null);\n      TtmlSubtitle ttmlSubtitle = null;\n      ArrayDeque<TtmlNode> nodeStack = new ArrayDeque<>();\n      int unsupportedNodeDepth = 0;\n      int eventType = xmlParser.getEventType();\n      FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;\n      CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;\n      TtsExtent ttsExtent = null;\n      while (eventType != XmlPullParser.END_DOCUMENT) {\n        TtmlNode parent = nodeStack.peek();\n        if (unsupportedNodeDepth == 0) {\n          String name = xmlParser.getName();\n          if (eventType == XmlPullParser.START_TAG) {\n            if (TtmlNode.TAG_TT.equals(name)) {\n              frameAndTickRate = parseFrameAndTickRates(xmlParser);\n              cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);\n              ttsExtent = parseTtsExtent(xmlParser);\n            }\n            if (!isSupportedTag(name)) {\n              Log.i(TAG, \"Ignoring unsupported tag: \" + xmlParser.getName());\n              unsupportedNodeDepth++;\n            } else if (TtmlNode.TAG_HEAD.equals(name)) {\n              parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap);\n            } else {\n              try {\n                TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);\n                nodeStack.push(node);\n                if (parent != null) {\n                  parent.addChild(node);\n                }\n              } catch (SubtitleDecoderException e) {\n                Log.w(TAG, \"Suppressing parser error\", e);\n                // Treat the node (and by extension, all of its children) as unsupported.\n                unsupportedNodeDepth++;\n              }\n            }\n          } else if (eventType == XmlPullParser.TEXT) {\n            parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));\n          } else if (eventType == XmlPullParser.END_TAG) {\n            if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {\n              ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);\n            }\n            nodeStack.pop();\n          }\n        } else {\n          if (eventType == XmlPullParser.START_TAG) {\n            unsupportedNodeDepth++;\n          } else if (eventType == XmlPullParser.END_TAG) {\n            unsupportedNodeDepth--;\n          }\n        }\n        xmlParser.next();\n        eventType = xmlParser.getEventType();\n      }\n      return ttmlSubtitle;\n    } catch (XmlPullParserException xppe) {\n      throw new SubtitleDecoderException(\"Unable to decode source\", xppe);\n    } catch (IOException e) {\n      throw new IllegalStateException(\"Unexpected error when reading input.\", e);\n    }\n  }\n\n  private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)\n      throws SubtitleDecoderException {\n    int frameRate = DEFAULT_FRAME_RATE;\n    String frameRateString = xmlParser.getAttributeValue(TTP, \"frameRate\");\n    if (frameRateString != null) {\n      frameRate = Integer.parseInt(frameRateString);\n    }\n\n    float frameRateMultiplier = 1;\n    String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, \"frameRateMultiplier\");\n    if (frameRateMultiplierString != null) {\n      String[] parts = Util.split(frameRateMultiplierString, \" \");\n      if (parts.length != 2) {\n        throw new SubtitleDecoderException(\"frameRateMultiplier doesn't have 2 parts\");\n      }\n      float numerator = Integer.parseInt(parts[0]);\n      float denominator = Integer.parseInt(parts[1]);\n      frameRateMultiplier = numerator / denominator;\n    }\n\n    int subFrameRate = DEFAULT_FRAME_AND_TICK_RATE.subFrameRate;\n    String subFrameRateString = xmlParser.getAttributeValue(TTP, \"subFrameRate\");\n    if (subFrameRateString != null) {\n      subFrameRate = Integer.parseInt(subFrameRateString);\n    }\n\n    int tickRate = DEFAULT_FRAME_AND_TICK_RATE.tickRate;\n    String tickRateString = xmlParser.getAttributeValue(TTP, \"tickRate\");\n    if (tickRateString != null) {\n      tickRate = Integer.parseInt(tickRateString);\n    }\n    return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);\n  }\n\n  private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)\n      throws SubtitleDecoderException {\n    String cellResolution = xmlParser.getAttributeValue(TTP, \"cellResolution\");\n    if (cellResolution == null) {\n      return defaultValue;\n    }\n\n    Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);\n    if (!cellResolutionMatcher.matches()) {\n      Log.w(TAG, \"Ignoring malformed cell resolution: \" + cellResolution);\n      return defaultValue;\n    }\n    try {\n      int columns = Integer.parseInt(cellResolutionMatcher.group(1));\n      int rows = Integer.parseInt(cellResolutionMatcher.group(2));\n      if (columns == 0 || rows == 0) {\n        throw new SubtitleDecoderException(\"Invalid cell resolution \" + columns + \" \" + rows);\n      }\n      return new CellResolution(columns, rows);\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed cell resolution: \" + cellResolution);\n      return defaultValue;\n    }\n  }\n\n  private TtsExtent parseTtsExtent(XmlPullParser xmlParser) {\n    String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);\n    if (ttsExtent == null) {\n      return null;\n    }\n\n    Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent);\n    if (!extentMatcher.matches()) {\n      Log.w(TAG, \"Ignoring non-pixel tts extent: \" + ttsExtent);\n      return null;\n    }\n    try {\n      int width = Integer.parseInt(extentMatcher.group(1));\n      int height = Integer.parseInt(extentMatcher.group(2));\n      return new TtsExtent(width, height);\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Ignoring malformed tts extent: \" + ttsExtent);\n      return null;\n    }\n  }\n\n  private Map<String, TtmlStyle> parseHeader(\n      XmlPullParser xmlParser,\n      Map<String, TtmlStyle> globalStyles,\n      CellResolution cellResolution,\n      TtsExtent ttsExtent,\n      Map<String, TtmlRegion> globalRegions,\n      Map<String, String> imageMap)\n      throws IOException, XmlPullParserException {\n    do {\n      xmlParser.next();\n      if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {\n        String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE);\n        TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());\n        if (parentStyleId != null) {\n          for (String id : parseStyleIds(parentStyleId)) {\n            style.chain(globalStyles.get(id));\n          }\n        }\n        if (style.getId() != null) {\n          globalStyles.put(style.getId(), style);\n        }\n      } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {\n        TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent);\n        if (ttmlRegion != null) {\n          globalRegions.put(ttmlRegion.id, ttmlRegion);\n        }\n      } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) {\n        parseMetadata(xmlParser, imageMap);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));\n    return globalStyles;\n  }\n\n  private void parseMetadata(XmlPullParser xmlParser, Map<String, String> imageMap)\n      throws IOException, XmlPullParserException {\n    do {\n      xmlParser.next();\n      if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) {\n        String id = XmlPullParserUtil.getAttributeValue(xmlParser, \"id\");\n        if (id != null) {\n          String encodedBitmapData = xmlParser.nextText();\n          imageMap.put(id, encodedBitmapData);\n        }\n      }\n    } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA));\n  }\n\n  /**\n   * Parses a region declaration.\n   *\n   * <p>Supports both percentage and pixel defined regions. In case of pixel defined regions the\n   * passed {@code ttsExtent} is used as a reference window to convert the pixel values to\n   * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is\n   * returned.\n   */\n  private TtmlRegion parseRegionAttributes(\n      XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) {\n    String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);\n    if (regionId == null) {\n      return null;\n    }\n\n    float position;\n    float line;\n\n    String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);\n    if (regionOrigin != null) {\n      Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);\n      Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin);\n      if (originPercentageMatcher.matches()) {\n        try {\n          position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f;\n          line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f;\n        } catch (NumberFormatException e) {\n          Log.w(TAG, \"Ignoring region with malformed origin: \" + regionOrigin);\n          return null;\n        }\n      } else if (originPixelMatcher.matches()) {\n        if (ttsExtent == null) {\n          Log.w(TAG, \"Ignoring region with missing tts:extent: \" + regionOrigin);\n          return null;\n        }\n        try {\n          int width = Integer.parseInt(originPixelMatcher.group(1));\n          int height = Integer.parseInt(originPixelMatcher.group(2));\n          // Convert pixel values to fractions.\n          position = width / (float) ttsExtent.width;\n          line = height / (float) ttsExtent.height;\n        } catch (NumberFormatException e) {\n          Log.w(TAG, \"Ignoring region with malformed origin: \" + regionOrigin);\n          return null;\n        }\n      } else {\n        Log.w(TAG, \"Ignoring region with unsupported origin: \" + regionOrigin);\n        return null;\n      }\n    } else {\n      Log.w(TAG, \"Ignoring region without an origin\");\n      return null;\n      // TODO: Should default to top left as below in this case, but need to fix\n      // https://github.com/google/ExoPlayer/issues/2953 first.\n      // Origin is omitted. Default to top left.\n      // position = 0;\n      // line = 0;\n    }\n\n    float width;\n    float height;\n    String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);\n    if (regionExtent != null) {\n      Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);\n      Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent);\n      if (extentPercentageMatcher.matches()) {\n        try {\n          width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f;\n          height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f;\n        } catch (NumberFormatException e) {\n          Log.w(TAG, \"Ignoring region with malformed extent: \" + regionOrigin);\n          return null;\n        }\n      } else if (extentPixelMatcher.matches()) {\n        if (ttsExtent == null) {\n          Log.w(TAG, \"Ignoring region with missing tts:extent: \" + regionOrigin);\n          return null;\n        }\n        try {\n          int extentWidth = Integer.parseInt(extentPixelMatcher.group(1));\n          int extentHeight = Integer.parseInt(extentPixelMatcher.group(2));\n          // Convert pixel values to fractions.\n          width = extentWidth / (float) ttsExtent.width;\n          height = extentHeight / (float) ttsExtent.height;\n        } catch (NumberFormatException e) {\n          Log.w(TAG, \"Ignoring region with malformed extent: \" + regionOrigin);\n          return null;\n        }\n      } else {\n        Log.w(TAG, \"Ignoring region with unsupported extent: \" + regionOrigin);\n        return null;\n      }\n    } else {\n      Log.w(TAG, \"Ignoring region without an extent\");\n      return null;\n      // TODO: Should default to extent of parent as below in this case, but need to fix\n      // https://github.com/google/ExoPlayer/issues/2953 first.\n      // Extent is omitted. Default to extent of parent.\n      // width = 1;\n      // height = 1;\n    }\n\n    @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START;\n    String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,\n        TtmlNode.ATTR_TTS_DISPLAY_ALIGN);\n    if (displayAlign != null) {\n      switch (Util.toLowerInvariant(displayAlign)) {\n        case \"center\":\n          lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;\n          line += height / 2;\n          break;\n        case \"after\":\n          lineAnchor = Cue.ANCHOR_TYPE_END;\n          line += height;\n          break;\n        default:\n          // Default \"before\" case. Do nothing.\n          break;\n      }\n    }\n\n    float regionTextHeight = 1.0f / cellResolution.rows;\n    return new TtmlRegion(\n        regionId,\n        position,\n        line,\n        /* lineType= */ Cue.LINE_TYPE_FRACTION,\n        lineAnchor,\n        width,\n        height,\n        /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,\n        /* textSize= */ regionTextHeight);\n  }\n\n  private String[] parseStyleIds(String parentStyleIds) {\n    parentStyleIds = parentStyleIds.trim();\n    return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, \"\\\\s+\");\n  }\n\n  private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {\n    int attributeCount = parser.getAttributeCount();\n    for (int i = 0; i < attributeCount; i++) {\n      String attributeValue = parser.getAttributeValue(i);\n      switch (parser.getAttributeName(i)) {\n        case TtmlNode.ATTR_ID:\n          if (TtmlNode.TAG_STYLE.equals(parser.getName())) {\n            style = createIfNull(style).setId(attributeValue);\n          }\n          break;\n        case TtmlNode.ATTR_TTS_BACKGROUND_COLOR:\n          style = createIfNull(style);\n          try {\n            style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));\n          } catch (IllegalArgumentException e) {\n            Log.w(TAG, \"Failed parsing background value: \" + attributeValue);\n          }\n          break;\n        case TtmlNode.ATTR_TTS_COLOR:\n          style = createIfNull(style);\n          try {\n            style.setFontColor(ColorParser.parseTtmlColor(attributeValue));\n          } catch (IllegalArgumentException e) {\n            Log.w(TAG, \"Failed parsing color value: \" + attributeValue);\n          }\n          break;\n        case TtmlNode.ATTR_TTS_FONT_FAMILY:\n          style = createIfNull(style).setFontFamily(attributeValue);\n          break;\n        case TtmlNode.ATTR_TTS_FONT_SIZE:\n          try {\n            style = createIfNull(style);\n            parseFontSize(attributeValue, style);\n          } catch (SubtitleDecoderException e) {\n            Log.w(TAG, \"Failed parsing fontSize value: \" + attributeValue);\n          }\n          break;\n        case TtmlNode.ATTR_TTS_FONT_WEIGHT:\n          style = createIfNull(style).setBold(\n              TtmlNode.BOLD.equalsIgnoreCase(attributeValue));\n          break;\n        case TtmlNode.ATTR_TTS_FONT_STYLE:\n          style = createIfNull(style).setItalic(\n              TtmlNode.ITALIC.equalsIgnoreCase(attributeValue));\n          break;\n        case TtmlNode.ATTR_TTS_TEXT_ALIGN:\n          switch (Util.toLowerInvariant(attributeValue)) {\n            case TtmlNode.LEFT:\n              style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);\n              break;\n            case TtmlNode.START:\n              style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);\n              break;\n            case TtmlNode.RIGHT:\n              style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);\n              break;\n            case TtmlNode.END:\n              style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);\n              break;\n            case TtmlNode.CENTER:\n              style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER);\n              break;\n          }\n          break;\n        case TtmlNode.ATTR_TTS_TEXT_DECORATION:\n          switch (Util.toLowerInvariant(attributeValue)) {\n            case TtmlNode.LINETHROUGH:\n              style = createIfNull(style).setLinethrough(true);\n              break;\n            case TtmlNode.NO_LINETHROUGH:\n              style = createIfNull(style).setLinethrough(false);\n              break;\n            case TtmlNode.UNDERLINE:\n              style = createIfNull(style).setUnderline(true);\n              break;\n            case TtmlNode.NO_UNDERLINE:\n              style = createIfNull(style).setUnderline(false);\n              break;\n          }\n          break;\n        default:\n          // ignore\n          break;\n      }\n    }\n    return style;\n  }\n\n  private TtmlStyle createIfNull(TtmlStyle style) {\n    return style == null ? new TtmlStyle() : style;\n  }\n\n  private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,\n      Map<String, TtmlRegion> regionMap, FrameAndTickRate frameAndTickRate)\n      throws SubtitleDecoderException {\n    long duration = C.TIME_UNSET;\n    long startTime = C.TIME_UNSET;\n    long endTime = C.TIME_UNSET;\n    String regionId = TtmlNode.ANONYMOUS_REGION_ID;\n    String imageId = null;\n    String[] styleIds = null;\n    int attributeCount = parser.getAttributeCount();\n    TtmlStyle style = parseStyleAttributes(parser, null);\n    for (int i = 0; i < attributeCount; i++) {\n      String attr = parser.getAttributeName(i);\n      String value = parser.getAttributeValue(i);\n      switch (attr) {\n        case ATTR_BEGIN:\n          startTime = parseTimeExpression(value, frameAndTickRate);\n          break;\n        case ATTR_END:\n          endTime = parseTimeExpression(value, frameAndTickRate);\n          break;\n        case ATTR_DURATION:\n          duration = parseTimeExpression(value, frameAndTickRate);\n          break;\n        case ATTR_STYLE:\n          // IDREFS: potentially multiple space delimited ids\n          String[] ids = parseStyleIds(value);\n          if (ids.length > 0) {\n            styleIds = ids;\n          }\n          break;\n        case ATTR_REGION:\n          if (regionMap.containsKey(value)) {\n            // If the region has not been correctly declared or does not define a position, we use\n            // the anonymous region.\n            regionId = value;\n          }\n          break;\n        case ATTR_IMAGE:\n          // Parse URI reference only if refers to an element in the same document (it must start\n          // with '#'). Resolving URIs from external sources is not supported.\n          if (value.startsWith(\"#\")) {\n            imageId = value.substring(1);\n          }\n          break;\n        default:\n          // Do nothing.\n          break;\n      }\n    }\n    if (parent != null && parent.startTimeUs != C.TIME_UNSET) {\n      if (startTime != C.TIME_UNSET) {\n        startTime += parent.startTimeUs;\n      }\n      if (endTime != C.TIME_UNSET) {\n        endTime += parent.startTimeUs;\n      }\n    }\n    if (endTime == C.TIME_UNSET) {\n      if (duration != C.TIME_UNSET) {\n        // Infer the end time from the duration.\n        endTime = startTime + duration;\n      } else if (parent != null && parent.endTimeUs != C.TIME_UNSET) {\n        // If the end time remains unspecified, then it should be inherited from the parent.\n        endTime = parent.endTimeUs;\n      }\n    }\n    return TtmlNode.buildNode(\n        parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);\n  }\n\n  private static boolean isSupportedTag(String tag) {\n    return tag.equals(TtmlNode.TAG_TT)\n        || tag.equals(TtmlNode.TAG_HEAD)\n        || tag.equals(TtmlNode.TAG_BODY)\n        || tag.equals(TtmlNode.TAG_DIV)\n        || tag.equals(TtmlNode.TAG_P)\n        || tag.equals(TtmlNode.TAG_SPAN)\n        || tag.equals(TtmlNode.TAG_BR)\n        || tag.equals(TtmlNode.TAG_STYLE)\n        || tag.equals(TtmlNode.TAG_STYLING)\n        || tag.equals(TtmlNode.TAG_LAYOUT)\n        || tag.equals(TtmlNode.TAG_REGION)\n        || tag.equals(TtmlNode.TAG_METADATA)\n        || tag.equals(TtmlNode.TAG_IMAGE)\n        || tag.equals(TtmlNode.TAG_DATA)\n        || tag.equals(TtmlNode.TAG_INFORMATION);\n  }\n\n  private static void parseFontSize(String expression, TtmlStyle out) throws\n      SubtitleDecoderException {\n    String[] expressions = Util.split(expression, \"\\\\s+\");\n    Matcher matcher;\n    if (expressions.length == 1) {\n      matcher = FONT_SIZE.matcher(expression);\n    } else if (expressions.length == 2){\n      matcher = FONT_SIZE.matcher(expressions[1]);\n      Log.w(TAG, \"Multiple values in fontSize attribute. Picking the second value for vertical font\"\n          + \" size and ignoring the first.\");\n    } else {\n      throw new SubtitleDecoderException(\"Invalid number of entries for fontSize: \"\n          + expressions.length + \".\");\n    }\n\n    if (matcher.matches()) {\n      String unit = matcher.group(3);\n      switch (unit) {\n        case \"px\":\n          out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL);\n          break;\n        case \"em\":\n          out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM);\n          break;\n        case \"%\":\n          out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PERCENT);\n          break;\n        default:\n          throw new SubtitleDecoderException(\"Invalid unit for fontSize: '\" + unit + \"'.\");\n      }\n      out.setFontSize(Float.valueOf(matcher.group(1)));\n    } else {\n      throw new SubtitleDecoderException(\"Invalid expression for fontSize: '\" + expression + \"'.\");\n    }\n  }\n\n  /**\n   * Parses a time expression, returning the parsed timestamp.\n   * <p>\n   * For the format of a time expression, see:\n   * <a href=\"http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression\">timeExpression</a>\n   *\n   * @param time A string that includes the time expression.\n   * @param frameAndTickRate The effective frame and tick rates of the stream.\n   * @return The parsed timestamp in microseconds.\n   * @throws SubtitleDecoderException If the given string does not contain a valid time expression.\n   */\n  private static long parseTimeExpression(String time, FrameAndTickRate frameAndTickRate)\n      throws SubtitleDecoderException {\n    Matcher matcher = CLOCK_TIME.matcher(time);\n    if (matcher.matches()) {\n      String hours = matcher.group(1);\n      double durationSeconds = Long.parseLong(hours) * 3600;\n      String minutes = matcher.group(2);\n      durationSeconds += Long.parseLong(minutes) * 60;\n      String seconds = matcher.group(3);\n      durationSeconds += Long.parseLong(seconds);\n      String fraction = matcher.group(4);\n      durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;\n      String frames = matcher.group(5);\n      durationSeconds += (frames != null)\n          ? Long.parseLong(frames) / frameAndTickRate.effectiveFrameRate : 0;\n      String subframes = matcher.group(6);\n      durationSeconds += (subframes != null)\n          ? ((double) Long.parseLong(subframes)) / frameAndTickRate.subFrameRate\n              / frameAndTickRate.effectiveFrameRate\n          : 0;\n      return (long) (durationSeconds * C.MICROS_PER_SECOND);\n    }\n    matcher = OFFSET_TIME.matcher(time);\n    if (matcher.matches()) {\n      String timeValue = matcher.group(1);\n      double offsetSeconds = Double.parseDouble(timeValue);\n      String unit = matcher.group(2);\n      switch (unit) {\n        case \"h\":\n          offsetSeconds *= 3600;\n          break;\n        case \"m\":\n          offsetSeconds *= 60;\n          break;\n        case \"s\":\n          // Do nothing.\n          break;\n        case \"ms\":\n          offsetSeconds /= 1000;\n          break;\n        case \"f\":\n          offsetSeconds /= frameAndTickRate.effectiveFrameRate;\n          break;\n        case \"t\":\n          offsetSeconds /= frameAndTickRate.tickRate;\n          break;\n      }\n      return (long) (offsetSeconds * C.MICROS_PER_SECOND);\n    }\n    throw new SubtitleDecoderException(\"Malformed time expression: \" + time);\n  }\n\n  private static final class FrameAndTickRate {\n    final float effectiveFrameRate;\n    final int subFrameRate;\n    final int tickRate;\n\n    FrameAndTickRate(float effectiveFrameRate, int subFrameRate, int tickRate) {\n      this.effectiveFrameRate = effectiveFrameRate;\n      this.subFrameRate = subFrameRate;\n      this.tickRate = tickRate;\n    }\n  }\n\n  /** Represents the cell resolution for a TTML file. */\n  private static final class CellResolution {\n    final int columns;\n    final int rows;\n\n    CellResolution(int columns, int rows) {\n      this.columns = columns;\n      this.rows = rows;\n    }\n  }\n\n  /** Represents the tts:extent for a TTML file. */\n  private static final class TtsExtent {\n    final int width;\n    final int height;\n\n    TtsExtent(int width, int height) {\n      this.width = width;\n      this.height = height;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.text.SpannableStringBuilder;\nimport android.util.Base64;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\n\n/**\n * A package internal representation of TTML node.\n */\n/* package */ final class TtmlNode {\n\n  public static final String TAG_TT = \"tt\";\n  public static final String TAG_HEAD = \"head\";\n  public static final String TAG_BODY = \"body\";\n  public static final String TAG_DIV = \"div\";\n  public static final String TAG_P = \"p\";\n  public static final String TAG_SPAN = \"span\";\n  public static final String TAG_BR = \"br\";\n  public static final String TAG_STYLE = \"style\";\n  public static final String TAG_STYLING = \"styling\";\n  public static final String TAG_LAYOUT = \"layout\";\n  public static final String TAG_REGION = \"region\";\n  public static final String TAG_METADATA = \"metadata\";\n  public static final String TAG_IMAGE = \"image\";\n  public static final String TAG_DATA = \"data\";\n  public static final String TAG_INFORMATION = \"information\";\n\n  public static final String ANONYMOUS_REGION_ID = \"\";\n  public static final String ATTR_ID = \"id\";\n  public static final String ATTR_TTS_ORIGIN = \"origin\";\n  public static final String ATTR_TTS_EXTENT = \"extent\";\n  public static final String ATTR_TTS_DISPLAY_ALIGN = \"displayAlign\";\n  public static final String ATTR_TTS_BACKGROUND_COLOR = \"backgroundColor\";\n  public static final String ATTR_TTS_FONT_STYLE = \"fontStyle\";\n  public static final String ATTR_TTS_FONT_SIZE = \"fontSize\";\n  public static final String ATTR_TTS_FONT_FAMILY = \"fontFamily\";\n  public static final String ATTR_TTS_FONT_WEIGHT = \"fontWeight\";\n  public static final String ATTR_TTS_COLOR = \"color\";\n  public static final String ATTR_TTS_TEXT_DECORATION = \"textDecoration\";\n  public static final String ATTR_TTS_TEXT_ALIGN = \"textAlign\";\n\n  public static final String LINETHROUGH = \"linethrough\";\n  public static final String NO_LINETHROUGH = \"nolinethrough\";\n  public static final String UNDERLINE = \"underline\";\n  public static final String NO_UNDERLINE = \"nounderline\";\n  public static final String ITALIC = \"italic\";\n  public static final String BOLD = \"bold\";\n\n  public static final String LEFT = \"left\";\n  public static final String CENTER = \"center\";\n  public static final String RIGHT = \"right\";\n  public static final String START = \"start\";\n  public static final String END = \"end\";\n\n  @Nullable public final String tag;\n  @Nullable public final String text;\n  public final boolean isTextNode;\n  public final long startTimeUs;\n  public final long endTimeUs;\n  @Nullable public final TtmlStyle style;\n  @Nullable private final String[] styleIds;\n  public final String regionId;\n  @Nullable public final String imageId;\n\n  private final HashMap<String, Integer> nodeStartsByRegion;\n  private final HashMap<String, Integer> nodeEndsByRegion;\n\n  private List<TtmlNode> children;\n\n  public static TtmlNode buildTextNode(String text) {\n    return new TtmlNode(\n        /* tag= */ null,\n        TtmlRenderUtil.applyTextElementSpacePolicy(text),\n        /* startTimeUs= */ C.TIME_UNSET,\n        /* endTimeUs= */ C.TIME_UNSET,\n        /* style= */ null,\n        /* styleIds= */ null,\n        ANONYMOUS_REGION_ID,\n        /* imageId= */ null);\n  }\n\n  public static TtmlNode buildNode(\n      @Nullable String tag,\n      long startTimeUs,\n      long endTimeUs,\n      @Nullable TtmlStyle style,\n      @Nullable String[] styleIds,\n      String regionId,\n      @Nullable String imageId) {\n    return new TtmlNode(\n        tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);\n  }\n\n  private TtmlNode(\n      @Nullable String tag,\n      @Nullable String text,\n      long startTimeUs,\n      long endTimeUs,\n      @Nullable TtmlStyle style,\n      @Nullable String[] styleIds,\n      String regionId,\n      @Nullable String imageId) {\n    this.tag = tag;\n    this.text = text;\n    this.imageId = imageId;\n    this.style = style;\n    this.styleIds = styleIds;\n    this.isTextNode = text != null;\n    this.startTimeUs = startTimeUs;\n    this.endTimeUs = endTimeUs;\n    this.regionId = Assertions.checkNotNull(regionId);\n    nodeStartsByRegion = new HashMap<>();\n    nodeEndsByRegion = new HashMap<>();\n  }\n\n  public boolean isActive(long timeUs) {\n    return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)\n        || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)\n        || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)\n        || (startTimeUs <= timeUs && timeUs < endTimeUs);\n  }\n\n  public void addChild(TtmlNode child) {\n    if (children == null) {\n      children = new ArrayList<>();\n    }\n    children.add(child);\n  }\n\n  public TtmlNode getChild(int index) {\n    if (children == null) {\n      throw new IndexOutOfBoundsException();\n    }\n    return children.get(index);\n  }\n\n  public int getChildCount() {\n    return children == null ? 0 : children.size();\n  }\n\n  public long[] getEventTimesUs() {\n    TreeSet<Long> eventTimeSet = new TreeSet<>();\n    getEventTimes(eventTimeSet, false);\n    long[] eventTimes = new long[eventTimeSet.size()];\n    int i = 0;\n    for (long eventTimeUs : eventTimeSet) {\n      eventTimes[i++] = eventTimeUs;\n    }\n    return eventTimes;\n  }\n\n  private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {\n    boolean isPNode = TAG_P.equals(tag);\n    boolean isDivNode = TAG_DIV.equals(tag);\n    if (descendsPNode || isPNode || (isDivNode && imageId != null)) {\n      if (startTimeUs != C.TIME_UNSET) {\n        out.add(startTimeUs);\n      }\n      if (endTimeUs != C.TIME_UNSET) {\n        out.add(endTimeUs);\n      }\n    }\n    if (children == null) {\n      return;\n    }\n    for (int i = 0; i < children.size(); i++) {\n      children.get(i).getEventTimes(out, descendsPNode || isPNode);\n    }\n  }\n\n  public String[] getStyleIds() {\n    return styleIds;\n  }\n\n  public List<Cue> getCues(\n      long timeUs,\n      Map<String, TtmlStyle> globalStyles,\n      Map<String, TtmlRegion> regionMap,\n      Map<String, String> imageMap) {\n\n    List<Pair<String, String>> regionImageOutputs = new ArrayList<>();\n    traverseForImage(timeUs, regionId, regionImageOutputs);\n\n    TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();\n    traverseForText(timeUs, false, regionId, regionTextOutputs);\n    traverseForStyle(timeUs, globalStyles, regionTextOutputs);\n\n    List<Cue> cues = new ArrayList<>();\n\n    // Create image based cues.\n    for (Pair<String, String> regionImagePair : regionImageOutputs) {\n      String encodedBitmapData = imageMap.get(regionImagePair.second);\n      if (encodedBitmapData == null) {\n        // Image reference points to an invalid image. Do nothing.\n        continue;\n      }\n\n      byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT);\n      Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length);\n      TtmlRegion region = regionMap.get(regionImagePair.first);\n\n      cues.add(\n          new Cue(\n              bitmap,\n              region.position,\n              Cue.ANCHOR_TYPE_START,\n              region.line,\n              region.lineAnchor,\n              region.width,\n              region.height));\n    }\n\n    // Create text based cues.\n    for (Entry<String, SpannableStringBuilder> entry : regionTextOutputs.entrySet()) {\n      TtmlRegion region = regionMap.get(entry.getKey());\n      cues.add(\n          new Cue(\n              cleanUpText(entry.getValue()),\n              /* textAlignment= */ null,\n              region.line,\n              region.lineType,\n              region.lineAnchor,\n              region.position,\n              /* positionAnchor= */ Cue.TYPE_UNSET,\n              region.width,\n              region.textSizeType,\n              region.textSize));\n    }\n\n    return cues;\n  }\n\n  private void traverseForImage(\n      long timeUs, String inheritedRegion, List<Pair<String, String>> regionImageList) {\n    String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;\n    if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) {\n      regionImageList.add(new Pair<>(resolvedRegionId, imageId));\n      return;\n    }\n    for (int i = 0; i < getChildCount(); ++i) {\n      getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList);\n    }\n  }\n\n  private void traverseForText(\n      long timeUs,\n      boolean descendsPNode,\n      String inheritedRegion,\n      Map<String, SpannableStringBuilder> regionOutputs) {\n    nodeStartsByRegion.clear();\n    nodeEndsByRegion.clear();\n    if (TAG_METADATA.equals(tag)) {\n      // Ignore metadata tag.\n      return;\n    }\n\n    String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;\n\n    if (isTextNode && descendsPNode) {\n      getRegionOutput(resolvedRegionId, regionOutputs).append(text);\n    } else if (TAG_BR.equals(tag) && descendsPNode) {\n      getRegionOutput(resolvedRegionId, regionOutputs).append('\\n');\n    } else if (isActive(timeUs)) {\n      // This is a container node, which can contain zero or more children.\n      for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {\n        nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());\n      }\n\n      boolean isPNode = TAG_P.equals(tag);\n      for (int i = 0; i < getChildCount(); i++) {\n        getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,\n            regionOutputs);\n      }\n      if (isPNode) {\n        TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));\n      }\n\n      for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {\n        nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());\n      }\n    }\n  }\n\n  private static SpannableStringBuilder getRegionOutput(\n      String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {\n    if (!regionOutputs.containsKey(resolvedRegionId)) {\n      regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());\n    }\n    return regionOutputs.get(resolvedRegionId);\n  }\n\n  private void traverseForStyle(\n      long timeUs,\n      Map<String, TtmlStyle> globalStyles,\n      Map<String, SpannableStringBuilder> regionOutputs) {\n    if (!isActive(timeUs)) {\n      return;\n    }\n    for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {\n      String regionId = entry.getKey();\n      int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;\n      int end = entry.getValue();\n      if (start != end) {\n        SpannableStringBuilder regionOutput = regionOutputs.get(regionId);\n        applyStyleToOutput(globalStyles, regionOutput, start, end);\n      }\n    }\n    for (int i = 0; i < getChildCount(); ++i) {\n      getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);\n    }\n  }\n\n  private void applyStyleToOutput(\n      Map<String, TtmlStyle> globalStyles,\n      SpannableStringBuilder regionOutput,\n      int start,\n      int end) {\n    TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);\n    if (resolvedStyle != null) {\n      TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);\n    }\n  }\n\n  private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {\n    // Having joined the text elements, we need to do some final cleanup on the result.\n    // 1. Collapse multiple consecutive spaces into a single space.\n    int builderLength = builder.length();\n    for (int i = 0; i < builderLength; i++) {\n      if (builder.charAt(i) == ' ') {\n        int j = i + 1;\n        while (j < builder.length() && builder.charAt(j) == ' ') {\n          j++;\n        }\n        int spacesToDelete = j - (i + 1);\n        if (spacesToDelete > 0) {\n          builder.delete(i, i + spacesToDelete);\n          builderLength -= spacesToDelete;\n        }\n      }\n    }\n    // 2. Remove any spaces from the start of each line.\n    if (builderLength > 0 && builder.charAt(0) == ' ') {\n      builder.delete(0, 1);\n      builderLength--;\n    }\n    for (int i = 0; i < builderLength - 1; i++) {\n      if (builder.charAt(i) == '\\n' && builder.charAt(i + 1) == ' ') {\n        builder.delete(i + 1, i + 2);\n        builderLength--;\n      }\n    }\n    // 3. Remove any spaces from the end of each line.\n    if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {\n      builder.delete(builderLength - 1, builderLength);\n      builderLength--;\n    }\n    for (int i = 0; i < builderLength - 1; i++) {\n      if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\\n') {\n        builder.delete(i, i + 1);\n        builderLength--;\n      }\n    }\n    // 4. Trim a trailing newline, if there is one.\n    if (builderLength > 0 && builder.charAt(builderLength - 1) == '\\n') {\n      builder.delete(builderLength - 1, builderLength);\n      /*builderLength--;*/\n    }\n    return builder;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport com.google.android.exoplayer2.text.Cue;\n\n/**\n * Represents a TTML Region.\n */\n/* package */ final class TtmlRegion {\n\n  public final String id;\n  public final float position;\n  public final float line;\n  public final @Cue.LineType int lineType;\n  public final @Cue.AnchorType int lineAnchor;\n  public final float width;\n  public final float height;\n  public final @Cue.TextSizeType int textSizeType;\n  public final float textSize;\n\n  public TtmlRegion(String id) {\n    this(\n        id,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* line= */ Cue.DIMEN_UNSET,\n        /* lineType= */ Cue.TYPE_UNSET,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* width= */ Cue.DIMEN_UNSET,\n        /* height= */ Cue.DIMEN_UNSET,\n        /* textSizeType= */ Cue.TYPE_UNSET,\n        /* textSize= */ Cue.DIMEN_UNSET);\n  }\n\n  public TtmlRegion(\n      String id,\n      float position,\n      float line,\n      @Cue.LineType int lineType,\n      @Cue.AnchorType int lineAnchor,\n      float width,\n      float height,\n      int textSizeType,\n      float textSize) {\n    this.id = id;\n    this.position = position;\n    this.line = line;\n    this.lineType = lineType;\n    this.lineAnchor = lineAnchor;\n    this.width = width;\n    this.height = height;\n    this.textSizeType = textSizeType;\n    this.textSize = textSize;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport android.text.Spannable;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.AlignmentSpan;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.text.style.StrikethroughSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport java.util.Map;\n\n/**\n * Package internal utility class to render styled <code>TtmlNode</code>s.\n */\n/* package */ final class TtmlRenderUtil {\n\n  public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,\n      Map<String, TtmlStyle> globalStyles) {\n    if (style == null && styleIds == null) {\n      // No styles at all.\n      return null;\n    } else if (style == null && styleIds.length == 1) {\n      // Only one single referential style present.\n      return globalStyles.get(styleIds[0]);\n    } else if (style == null && styleIds.length > 1) {\n      // Only multiple referential styles present.\n      TtmlStyle chainedStyle = new TtmlStyle();\n      for (String id : styleIds) {\n        chainedStyle.chain(globalStyles.get(id));\n      }\n      return chainedStyle;\n    } else if (style != null && styleIds != null && styleIds.length == 1) {\n      // Merge a single referential style into inline style.\n      return style.chain(globalStyles.get(styleIds[0]));\n    } else if (style != null && styleIds != null && styleIds.length > 1) {\n      // Merge multiple referential styles into inline style.\n      for (String id : styleIds) {\n        style.chain(globalStyles.get(id));\n      }\n      return style;\n    }\n    // Only inline styles available.\n    return style;\n  }\n\n  public static void applyStylesToSpan(SpannableStringBuilder builder,\n      int start, int end, TtmlStyle style) {\n\n    if (style.getStyle() != TtmlStyle.UNSPECIFIED) {\n      builder.setSpan(new StyleSpan(style.getStyle()), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.isLinethrough()) {\n      builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.isUnderline()) {\n      builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.hasFontColor()) {\n      builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,\n          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.hasBackgroundColor()) {\n      builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,\n          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.getFontFamily() != null) {\n      builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.getTextAlign() != null) {\n      builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    switch (style.getFontSizeUnit()) {\n      case TtmlStyle.FONT_SIZE_UNIT_PIXEL:\n        builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TtmlStyle.FONT_SIZE_UNIT_EM:\n        builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TtmlStyle.FONT_SIZE_UNIT_PERCENT:\n        builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TtmlStyle.UNSPECIFIED:\n        // Do nothing.\n        break;\n    }\n  }\n\n  /**\n   * Called when the end of a paragraph is encountered. Adds a newline if there are one or more\n   * non-space characters since the previous newline.\n   *\n   * @param builder The builder.\n   */\n  /* package */ static void endParagraph(SpannableStringBuilder builder) {\n    int position = builder.length() - 1;\n    while (position >= 0 && builder.charAt(position) == ' ') {\n      position--;\n    }\n    if (position >= 0 && builder.charAt(position) != '\\n') {\n      builder.append('\\n');\n    }\n  }\n\n  /**\n   * Applies the appropriate space policy to the given text element.\n   *\n   * @param in The text element to which the policy should be applied.\n   * @return The result of applying the policy to the text element.\n   */\n  /* package */ static String applyTextElementSpacePolicy(String in) {\n    // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends\n    String out = in.replaceAll(\"\\r\\n\", \"\\n\");\n    // Apply suppress-at-line-break=\"auto\" and\n    // white-space-treatment=\"ignore-if-surrounding-linefeed\"\n    out = out.replaceAll(\" *\\n *\", \"\\n\");\n    // Apply linefeed-treatment=\"treat-as-space\"\n    out = out.replaceAll(\"\\n\", \" \");\n    // Apply white-space-collapse=\"true\"\n    out = out.replaceAll(\"[ \\t\\\\x0B\\f\\r]+\", \" \");\n    return out;\n  }\n\n  private TtmlRenderUtil() {}\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport android.graphics.Typeface;\nimport android.text.Layout;\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Style object of a <code>TtmlNode</code>\n */\n/* package */ final class TtmlStyle {\n\n  public static final int UNSPECIFIED = -1;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC})\n  public @interface StyleFlags {}\n\n  public static final int STYLE_NORMAL = Typeface.NORMAL;\n  public static final int STYLE_BOLD = Typeface.BOLD;\n  public static final int STYLE_ITALIC = Typeface.ITALIC;\n  public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})\n  public @interface FontSizeUnit {}\n\n  public static final int FONT_SIZE_UNIT_PIXEL = 1;\n  public static final int FONT_SIZE_UNIT_EM = 2;\n  public static final int FONT_SIZE_UNIT_PERCENT = 3;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({UNSPECIFIED, OFF, ON})\n  private @interface OptionalBoolean {}\n\n  private static final int OFF = 0;\n  private static final int ON = 1;\n\n  private String fontFamily;\n  private int fontColor;\n  private boolean hasFontColor;\n  private int backgroundColor;\n  private boolean hasBackgroundColor;\n  @OptionalBoolean private int linethrough;\n  @OptionalBoolean private int underline;\n  @OptionalBoolean private int bold;\n  @OptionalBoolean private int italic;\n  @FontSizeUnit private int fontSizeUnit;\n  private float fontSize;\n  private String id;\n  private TtmlStyle inheritableStyle;\n  private Layout.Alignment textAlign;\n\n  public TtmlStyle() {\n    linethrough = UNSPECIFIED;\n    underline = UNSPECIFIED;\n    bold = UNSPECIFIED;\n    italic = UNSPECIFIED;\n    fontSizeUnit = UNSPECIFIED;\n  }\n\n  /**\n   * Returns the style or {@link #UNSPECIFIED} when no style information is given.\n   *\n   * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}\n   *     or {@link #STYLE_BOLD_ITALIC}.\n   */\n  @StyleFlags public int getStyle() {\n    if (bold == UNSPECIFIED && italic == UNSPECIFIED) {\n      return UNSPECIFIED;\n    }\n    return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)\n        | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);\n  }\n\n  public boolean isLinethrough() {\n    return linethrough == ON;\n  }\n\n  public TtmlStyle setLinethrough(boolean linethrough) {\n    Assertions.checkState(inheritableStyle == null);\n    this.linethrough = linethrough ? ON : OFF;\n    return this;\n  }\n\n  public boolean isUnderline() {\n    return underline == ON;\n  }\n\n  public TtmlStyle setUnderline(boolean underline) {\n    Assertions.checkState(inheritableStyle == null);\n    this.underline = underline ? ON : OFF;\n    return this;\n  }\n\n  public TtmlStyle setBold(boolean bold) {\n    Assertions.checkState(inheritableStyle == null);\n    this.bold = bold ? ON : OFF;\n    return this;\n  }\n\n  public TtmlStyle setItalic(boolean italic) {\n    Assertions.checkState(inheritableStyle == null);\n    this.italic = italic ? ON : OFF;\n    return this;\n  }\n\n  public String getFontFamily() {\n    return fontFamily;\n  }\n\n  public TtmlStyle setFontFamily(String fontFamily) {\n    Assertions.checkState(inheritableStyle == null);\n    this.fontFamily = fontFamily;\n    return this;\n  }\n\n  public int getFontColor() {\n    if (!hasFontColor) {\n      throw new IllegalStateException(\"Font color has not been defined.\");\n    }\n    return fontColor;\n  }\n\n  public TtmlStyle setFontColor(int fontColor) {\n    Assertions.checkState(inheritableStyle == null);\n    this.fontColor = fontColor;\n    hasFontColor = true;\n    return this;\n  }\n\n  public boolean hasFontColor() {\n    return hasFontColor;\n  }\n\n  public int getBackgroundColor() {\n    if (!hasBackgroundColor) {\n      throw new IllegalStateException(\"Background color has not been defined.\");\n    }\n    return backgroundColor;\n  }\n\n  public TtmlStyle setBackgroundColor(int backgroundColor) {\n    this.backgroundColor = backgroundColor;\n    hasBackgroundColor = true;\n    return this;\n  }\n\n  public boolean hasBackgroundColor() {\n    return hasBackgroundColor;\n  }\n\n  /**\n   * Inherits from an ancestor style. Properties like <i>tts:backgroundColor</i> which\n   * are not inheritable are not inherited as well as properties which are already set locally\n   * are never overridden.\n   *\n   * @param ancestor the ancestor style to inherit from\n   */\n  public TtmlStyle inherit(TtmlStyle ancestor) {\n    return inherit(ancestor, false);\n  }\n\n  /**\n   * Chains this style to referential style. Local properties which are already set\n   * are never overridden.\n   *\n   * @param ancestor the referential style to inherit from\n   */\n  public TtmlStyle chain(TtmlStyle ancestor) {\n    return inherit(ancestor, true);\n  }\n\n  private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) {\n    if (ancestor != null) {\n      if (!hasFontColor && ancestor.hasFontColor) {\n        setFontColor(ancestor.fontColor);\n      }\n      if (bold == UNSPECIFIED) {\n        bold = ancestor.bold;\n      }\n      if (italic == UNSPECIFIED) {\n        italic = ancestor.italic;\n      }\n      if (fontFamily == null) {\n        fontFamily = ancestor.fontFamily;\n      }\n      if (linethrough == UNSPECIFIED) {\n        linethrough = ancestor.linethrough;\n      }\n      if (underline == UNSPECIFIED) {\n        underline = ancestor.underline;\n      }\n      if (textAlign == null) {\n        textAlign = ancestor.textAlign;\n      }\n      if (fontSizeUnit == UNSPECIFIED) {\n        fontSizeUnit = ancestor.fontSizeUnit;\n        fontSize = ancestor.fontSize;\n      }\n      // attributes not inherited as of http://www.w3.org/TR/ttml1/\n      if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {\n        setBackgroundColor(ancestor.backgroundColor);\n      }\n    }\n    return this;\n  }\n\n  public TtmlStyle setId(String id) {\n    this.id = id;\n    return this;\n  }\n\n  public String getId() {\n    return id;\n  }\n\n  public Layout.Alignment getTextAlign() {\n    return textAlign;\n  }\n\n  public TtmlStyle setTextAlign(Layout.Alignment textAlign) {\n    this.textAlign = textAlign;\n    return this;\n  }\n\n  public TtmlStyle setFontSize(float fontSize) {\n    this.fontSize = fontSize;\n    return this;\n  }\n\n  public TtmlStyle setFontSizeUnit(int fontSizeUnit) {\n    this.fontSizeUnit = fontSizeUnit;\n    return this;\n  }\n\n  @FontSizeUnit public int getFontSizeUnit() {\n    return fontSizeUnit;\n  }\n\n  public float getFontSize() {\n    return fontSize;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.ttml;\n\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A representation of a TTML subtitle.\n */\n/* package */ final class TtmlSubtitle implements Subtitle {\n\n  private final TtmlNode root;\n  private final long[] eventTimesUs;\n  private final Map<String, TtmlStyle> globalStyles;\n  private final Map<String, TtmlRegion> regionMap;\n  private final Map<String, String> imageMap;\n\n  public TtmlSubtitle(\n      TtmlNode root,\n      Map<String, TtmlStyle> globalStyles,\n      Map<String, TtmlRegion> regionMap,\n      Map<String, String> imageMap) {\n    this.root = root;\n    this.regionMap = regionMap;\n    this.imageMap = imageMap;\n    this.globalStyles =\n        globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();\n    this.eventTimesUs = root.getEventTimesUs();\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false);\n    return index < eventTimesUs.length ? index : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return eventTimesUs.length;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    return eventTimesUs[index];\n  }\n\n  @VisibleForTesting\n  /* package */ TtmlNode getRoot() {\n    return root;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return root.getCues(timeUs, globalStyles, regionMap, imageMap);\n  }\n\n  @VisibleForTesting\n  /* package */ Map<String, TtmlStyle> getGlobalStyles() {\n    return globalStyles;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.ttml;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.tx3g;\n\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.charset.Charset;\nimport java.util.List;\n\n/**\n * A {@link SimpleSubtitleDecoder} for tx3g.\n * <p>\n * Currently supports parsing of a single text track with embedded styles.\n */\npublic final class Tx3gDecoder extends SimpleSubtitleDecoder {\n\n  private static final char BOM_UTF16_BE = '\\uFEFF';\n  private static final char BOM_UTF16_LE = '\\uFFFE';\n\n  private static final int TYPE_STYL = 0x7374796c;\n  private static final int TYPE_TBOX = 0x74626f78;\n  private static final String TX3G_SERIF = \"Serif\";\n\n  private static final int SIZE_ATOM_HEADER = 8;\n  private static final int SIZE_SHORT = 2;\n  private static final int SIZE_BOM_UTF16 = 2;\n  private static final int SIZE_STYLE_RECORD = 12;\n\n  private static final int FONT_FACE_BOLD = 0x0001;\n  private static final int FONT_FACE_ITALIC = 0x0002;\n  private static final int FONT_FACE_UNDERLINE = 0x0004;\n\n  private static final int SPAN_PRIORITY_LOW = 0xFF << Spanned.SPAN_PRIORITY_SHIFT;\n  private static final int SPAN_PRIORITY_HIGH = 0;\n\n  private static final int DEFAULT_FONT_FACE = 0;\n  private static final int DEFAULT_COLOR = Color.WHITE;\n  private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME;\n  private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;\n\n  private final ParsableByteArray parsableByteArray;\n\n  private boolean customVerticalPlacement;\n  private int defaultFontFace;\n  private int defaultColorRgba;\n  private String defaultFontFamily;\n  private float defaultVerticalPlacement;\n  private int calculatedVideoTrackHeight;\n\n  /**\n   * Sets up a new {@link Tx3gDecoder} with default values.\n   *\n   * @param initializationData Sample description atom ('stsd') data with default subtitle styles.\n   */\n  public Tx3gDecoder(List<byte[]> initializationData) {\n    super(\"Tx3gDecoder\");\n    parsableByteArray = new ParsableByteArray();\n\n    if (initializationData != null && initializationData.size() == 1\n        && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {\n      byte[] initializationBytes = initializationData.get(0);\n      defaultFontFace = initializationBytes[24];\n      defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24)\n          | ((initializationBytes[27] & 0xFF) << 16)\n          | ((initializationBytes[28] & 0xFF) << 8)\n          | (initializationBytes[29] & 0xFF);\n      String fontFamily =\n          Util.fromUtf8Bytes(initializationBytes, 43, initializationBytes.length - 43);\n      defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME;\n      //font size (initializationBytes[25]) is 5% of video height\n      calculatedVideoTrackHeight = 20 * initializationBytes[25];\n      customVerticalPlacement = (initializationBytes[0] & 0x20) != 0;\n      if (customVerticalPlacement) {\n        int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8)\n            | (initializationBytes[11] & 0xFF);\n        defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;\n        defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f);\n      } else {\n        defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;\n      }\n    } else {\n      defaultFontFace = DEFAULT_FONT_FACE;\n      defaultColorRgba = DEFAULT_COLOR;\n      defaultFontFamily = DEFAULT_FONT_FAMILY;\n      customVerticalPlacement = false;\n      defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;\n    }\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset)\n      throws SubtitleDecoderException {\n    parsableByteArray.reset(bytes, length);\n    String cueTextString = readSubtitleText(parsableByteArray);\n    if (cueTextString.isEmpty()) {\n      return Tx3gSubtitle.EMPTY;\n    }\n    // Attach default styles.\n    SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString);\n    attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(),\n        SPAN_PRIORITY_LOW);\n    attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(),\n        SPAN_PRIORITY_LOW);\n    attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(),\n        SPAN_PRIORITY_LOW);\n    float verticalPlacement = defaultVerticalPlacement;\n    // Find and attach additional styles.\n    while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) {\n      int position = parsableByteArray.getPosition();\n      int atomSize = parsableByteArray.readInt();\n      int atomType = parsableByteArray.readInt();\n      if (atomType == TYPE_STYL) {\n        assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);\n        int styleRecordCount = parsableByteArray.readUnsignedShort();\n        for (int i = 0; i < styleRecordCount; i++) {\n          applyStyleRecord(parsableByteArray, cueText);\n        }\n      } else if (atomType == TYPE_TBOX && customVerticalPlacement) {\n        assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);\n        int requestedVerticalPlacement = parsableByteArray.readUnsignedShort();\n        verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;\n        verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f);\n      }\n      parsableByteArray.setPosition(position + atomSize);\n    }\n    return new Tx3gSubtitle(\n        new Cue(\n            cueText,\n            /* textAlignment= */ null,\n            verticalPlacement,\n            Cue.LINE_TYPE_FRACTION,\n            Cue.ANCHOR_TYPE_START,\n            Cue.DIMEN_UNSET,\n            Cue.TYPE_UNSET,\n            Cue.DIMEN_UNSET));\n  }\n\n  private static String readSubtitleText(ParsableByteArray parsableByteArray)\n      throws SubtitleDecoderException {\n    assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);\n    int textLength = parsableByteArray.readUnsignedShort();\n    if (textLength == 0) {\n      return \"\";\n    }\n    if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) {\n      char firstChar = parsableByteArray.peekChar();\n      if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) {\n        return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME));\n      }\n    }\n    return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME));\n  }\n\n  private void applyStyleRecord(ParsableByteArray parsableByteArray,\n      SpannableStringBuilder cueText) throws SubtitleDecoderException {\n    assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD);\n    int start = parsableByteArray.readUnsignedShort();\n    int end = parsableByteArray.readUnsignedShort();\n    parsableByteArray.skipBytes(2); // font identifier\n    int fontFace = parsableByteArray.readUnsignedByte();\n    parsableByteArray.skipBytes(1); // font size\n    int colorRgba = parsableByteArray.readInt();\n    attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);\n    attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);\n  }\n\n  private static void attachFontFace(SpannableStringBuilder cueText, int fontFace,\n      int defaultFontFace, int start, int end, int spanPriority) {\n    if (fontFace != defaultFontFace) {\n      final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority;\n      boolean isBold = (fontFace & FONT_FACE_BOLD) != 0;\n      boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0;\n      if (isBold) {\n        if (isItalic) {\n          cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags);\n        } else {\n          cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags);\n        }\n      } else if (isItalic) {\n        cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags);\n      }\n      boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0;\n      if (isUnderlined) {\n        cueText.setSpan(new UnderlineSpan(), start, end, flags);\n      }\n      if (!isUnderlined && !isBold && !isItalic) {\n        cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags);\n      }\n    }\n  }\n\n  private static void attachColor(SpannableStringBuilder cueText, int colorRgba,\n      int defaultColorRgba, int start, int end, int spanPriority) {\n    if (colorRgba != defaultColorRgba) {\n      int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8);\n      cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);\n    }\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily,\n      String defaultFontFamily, int start, int end, int spanPriority) {\n    if (fontFamily != defaultFontFamily) {\n      cueText.setSpan(new TypefaceSpan(fontFamily), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);\n    }\n  }\n\n  private static void assertTrue(boolean checkValue) throws SubtitleDecoderException {\n    if (!checkValue) {\n      throw new SubtitleDecoderException(\"Unexpected subtitle format.\");\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.tx3g;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A representation of a tx3g subtitle.\n */\n/* package */ final class Tx3gSubtitle implements Subtitle {\n\n  public static final Tx3gSubtitle EMPTY = new Tx3gSubtitle();\n\n  private final List<Cue> cues;\n\n  public Tx3gSubtitle(Cue cue) {\n    this.cues = Collections.singletonList(cue);\n  }\n\n  private Tx3gSubtitle() {\n    this.cues = Collections.emptyList();\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return timeUs < 0 ? 0 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return 1;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index == 0);\n    return 0;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return timeUs >= 0 ? cues : Collections.emptyList();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.tx3g;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.ColorParser;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS\n * features.\n */\n/* package */ final class CssParser {\n\n  private static final String PROPERTY_BGCOLOR = \"background-color\";\n  private static final String PROPERTY_FONT_FAMILY = \"font-family\";\n  private static final String PROPERTY_FONT_WEIGHT = \"font-weight\";\n  private static final String PROPERTY_TEXT_DECORATION = \"text-decoration\";\n  private static final String VALUE_BOLD = \"bold\";\n  private static final String VALUE_UNDERLINE = \"underline\";\n  private static final String RULE_START = \"{\";\n  private static final String RULE_END = \"}\";\n  private static final String PROPERTY_FONT_STYLE = \"font-style\";\n  private static final String VALUE_ITALIC = \"italic\";\n\n  private static final Pattern VOICE_NAME_PATTERN = Pattern.compile(\"\\\\[voice=\\\"([^\\\"]*)\\\"\\\\]\");\n\n  // Temporary utility data structures.\n  private final ParsableByteArray styleInput;\n  private final StringBuilder stringBuilder;\n\n  public CssParser() {\n    styleInput = new ParsableByteArray();\n    stringBuilder = new StringBuilder();\n  }\n\n  /**\n   * Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents\n   * of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If\n   * parsing fails, it returns a list including only the styles which have been successfully parsed\n   * up to the style rule which was malformed.\n   *\n   * @param input The input from which the style block should be read.\n   * @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list\n   *     containing the styles up to the parsing failure.\n   */\n  public List<WebvttCssStyle> parseBlock(ParsableByteArray input) {\n    stringBuilder.setLength(0);\n    int initialInputPosition = input.getPosition();\n    skipStyleBlock(input);\n    styleInput.reset(input.data, input.getPosition());\n    styleInput.setPosition(initialInputPosition);\n\n    List<WebvttCssStyle> styles = new ArrayList<>();\n    String selector;\n    while ((selector = parseSelector(styleInput, stringBuilder)) != null) {\n      if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) {\n        return styles;\n      }\n      WebvttCssStyle style = new WebvttCssStyle();\n      applySelectorToStyle(style, selector);\n      String token = null;\n      boolean blockEndFound = false;\n      while (!blockEndFound) {\n        int position = styleInput.getPosition();\n        token = parseNextToken(styleInput, stringBuilder);\n        blockEndFound = token == null || RULE_END.equals(token);\n        if (!blockEndFound) {\n          styleInput.setPosition(position);\n          parseStyleDeclaration(styleInput, style, stringBuilder);\n        }\n      }\n      // Check that the style rule ended correctly.\n      if (RULE_END.equals(token)) {\n        styles.add(style);\n      }\n    }\n    return styles;\n  }\n\n  /**\n   * Returns a string containing the selector. The input is expected to have the form {@code\n   * ::cue(tag#id.class1.class2[voice=\"someone\"]}, where every element is optional.\n   *\n   * @param input From which the selector is obtained.\n   * @return A string containing the target, empty string if the selector is universal (targets all\n   *     cues) or null if an error was encountered.\n   */\n  @Nullable\n  private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    if (input.bytesLeft() < 5) {\n      return null;\n    }\n    String cueSelector = input.readString(5);\n    if (!\"::cue\".equals(cueSelector)) {\n      return null;\n    }\n    int position = input.getPosition();\n    String token = parseNextToken(input, stringBuilder);\n    if (token == null) {\n      return null;\n    }\n    if (RULE_START.equals(token)) {\n      input.setPosition(position);\n      return \"\";\n    }\n    String target = null;\n    if (\"(\".equals(token)) {\n      target = readCueTarget(input);\n    }\n    token = parseNextToken(input, stringBuilder);\n    if (!\")\".equals(token)) {\n      return null;\n    }\n    return target;\n  }\n\n  /**\n   * Reads the contents of ::cue() and returns it as a string.\n   */\n  private static String readCueTarget(ParsableByteArray input) {\n    int position = input.getPosition();\n    int limit = input.limit();\n    boolean cueTargetEndFound = false;\n    while (position < limit && !cueTargetEndFound) {\n      char c = (char) input.data[position++];\n      cueTargetEndFound = c == ')';\n    }\n    return input.readString(--position - input.getPosition()).trim();\n    // --offset to return ')' to the input.\n  }\n\n  private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,\n      StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    String property = parseIdentifier(input, stringBuilder);\n    if (\"\".equals(property)) {\n      return;\n    }\n    if (!\":\".equals(parseNextToken(input, stringBuilder))) {\n      return;\n    }\n    skipWhitespaceAndComments(input);\n    String value = parsePropertyValue(input, stringBuilder);\n    if (value == null || \"\".equals(value)) {\n      return;\n    }\n    int position = input.getPosition();\n    String token = parseNextToken(input, stringBuilder);\n    if (\";\".equals(token)) {\n      // The style declaration is well formed.\n    } else if (RULE_END.equals(token)) {\n      // The style declaration is well formed and we can go on, but the closing bracket had to be\n      // fed back.\n      input.setPosition(position);\n    } else {\n      // The style declaration is not well formed.\n      return;\n    }\n    // At this point we have a presumably valid declaration, we need to parse it and fill the style.\n    if (\"color\".equals(property)) {\n      style.setFontColor(ColorParser.parseCssColor(value));\n    } else if (PROPERTY_BGCOLOR.equals(property)) {\n      style.setBackgroundColor(ColorParser.parseCssColor(value));\n    } else if (PROPERTY_TEXT_DECORATION.equals(property)) {\n      if (VALUE_UNDERLINE.equals(value)) {\n        style.setUnderline(true);\n      }\n    } else if (PROPERTY_FONT_FAMILY.equals(property)) {\n      style.setFontFamily(value);\n    } else if (PROPERTY_FONT_WEIGHT.equals(property)) {\n      if (VALUE_BOLD.equals(value)) {\n        style.setBold(true);\n      }\n    } else if (PROPERTY_FONT_STYLE.equals(property)) {\n      if (VALUE_ITALIC.equals(value)) {\n        style.setItalic(true);\n      }\n    }\n    // TODO: Fill remaining supported styles.\n  }\n\n  // Visible for testing.\n  /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {\n    boolean skipping = true;\n    while (input.bytesLeft() > 0 && skipping) {\n      skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);\n    }\n  }\n\n  // Visible for testing.\n  @Nullable\n  /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    if (input.bytesLeft() == 0) {\n      return null;\n    }\n    String identifier = parseIdentifier(input, stringBuilder);\n    if (!\"\".equals(identifier)) {\n      return identifier;\n    }\n    // We found a delimiter.\n    return \"\" + (char) input.readUnsignedByte();\n  }\n\n  private static boolean maybeSkipWhitespace(ParsableByteArray input) {\n    switch(peekCharAtPosition(input, input.getPosition())) {\n      case '\\t':\n      case '\\r':\n      case '\\n':\n      case '\\f':\n      case ' ':\n        input.skipBytes(1);\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  // Visible for testing.\n  /* package */ static void skipStyleBlock(ParsableByteArray input) {\n    // The style block cannot contain empty lines, so we assume the input ends when a empty line\n    // is found.\n    String line;\n    do {\n      line = input.readLine();\n    } while (!TextUtils.isEmpty(line));\n  }\n\n  private static char peekCharAtPosition(ParsableByteArray input, int position) {\n    return (char) input.data[position];\n  }\n\n  @Nullable\n  private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {\n    StringBuilder expressionBuilder = new StringBuilder();\n    String token;\n    int position;\n    boolean expressionEndFound = false;\n    // TODO: Add support for \"Strings in quotes with spaces\".\n    while (!expressionEndFound) {\n      position = input.getPosition();\n      token = parseNextToken(input, stringBuilder);\n      if (token == null) {\n        // Syntax error.\n        return null;\n      }\n      if (RULE_END.equals(token) || \";\".equals(token)) {\n        input.setPosition(position);\n        expressionEndFound = true;\n      } else {\n        expressionBuilder.append(token);\n      }\n    }\n    return expressionBuilder.toString();\n  }\n\n  private static boolean maybeSkipComment(ParsableByteArray input) {\n    int position = input.getPosition();\n    int limit = input.limit();\n    byte[] data = input.data;\n    if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {\n      while (position + 1 < limit) {\n        char skippedChar = (char) data[position++];\n        if (skippedChar == '*') {\n          if (((char) data[position]) == '/') {\n            position++;\n            limit = position;\n          }\n        }\n      }\n      input.skipBytes(limit - input.getPosition());\n      return true;\n    }\n    return false;\n  }\n\n  private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {\n    stringBuilder.setLength(0);\n    int position = input.getPosition();\n    int limit = input.limit();\n    boolean identifierEndFound = false;\n    while (position  < limit && !identifierEndFound) {\n      char c = (char) input.data[position];\n      if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'\n          || c == '-' || c == '.' || c == '_') {\n        position++;\n        stringBuilder.append(c);\n      } else {\n        identifierEndFound = true;\n      }\n    }\n    input.skipBytes(position - input.getPosition());\n    return stringBuilder.toString();\n  }\n\n  /**\n   * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form\n   * {@code ::cue(tag#id.class1.class2[voice=\"someone\"]}, where every element is optional.\n   */\n  private void applySelectorToStyle(WebvttCssStyle style, String selector) {\n    if (\"\".equals(selector)) {\n      return; // Universal selector.\n    }\n    int voiceStartIndex = selector.indexOf('[');\n    if (voiceStartIndex != -1) {\n      Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));\n      if (matcher.matches()) {\n        style.setTargetVoice(matcher.group(1));\n      }\n      selector = selector.substring(0, voiceStartIndex);\n    }\n    String[] classDivision = Util.split(selector, \"\\\\.\");\n    String tagAndIdDivision = classDivision[0];\n    int idPrefixIndex = tagAndIdDivision.indexOf('#');\n    if (idPrefixIndex != -1) {\n      style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));\n      style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.\n    } else {\n      style.setTargetTagName(tagAndIdDivision);\n    }\n    if (classDivision.length > 1) {\n      style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length));\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */\n@SuppressWarnings(\"ConstantField\")\npublic final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {\n\n  private static final int BOX_HEADER_SIZE = 8;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_payl = 0x7061796c;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_sttg = 0x73747467;\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  private static final int TYPE_vttc = 0x76747463;\n\n  private final ParsableByteArray sampleData;\n  private final WebvttCue.Builder builder;\n\n  public Mp4WebvttDecoder() {\n    super(\"Mp4WebvttDecoder\");\n    sampleData = new ParsableByteArray();\n    builder = new WebvttCue.Builder();\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset)\n      throws SubtitleDecoderException {\n    // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:\n    // first 4 bytes size and then 4 bytes type.\n    sampleData.reset(bytes, length);\n    List<Cue> resultingCueList = new ArrayList<>();\n    while (sampleData.bytesLeft() > 0) {\n      if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {\n        throw new SubtitleDecoderException(\"Incomplete Mp4Webvtt Top Level box header found.\");\n      }\n      int boxSize = sampleData.readInt();\n      int boxType = sampleData.readInt();\n      if (boxType == TYPE_vttc) {\n        resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));\n      } else {\n        // Peers of the VTTCueBox are still not supported and are skipped.\n        sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);\n      }\n    }\n    return new Mp4WebvttSubtitle(resultingCueList);\n  }\n\n  private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder,\n        int remainingCueBoxBytes) throws SubtitleDecoderException {\n    builder.reset();\n    while (remainingCueBoxBytes > 0) {\n      if (remainingCueBoxBytes < BOX_HEADER_SIZE) {\n        throw new SubtitleDecoderException(\"Incomplete vtt cue box header found.\");\n      }\n      int boxSize = sampleData.readInt();\n      int boxType = sampleData.readInt();\n      remainingCueBoxBytes -= BOX_HEADER_SIZE;\n      int payloadLength = boxSize - BOX_HEADER_SIZE;\n      String boxPayload =\n          Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength);\n      sampleData.skipBytes(payloadLength);\n      remainingCueBoxBytes -= payloadLength;\n      if (boxType == TYPE_sttg) {\n        WebvttCueParser.parseCueSettingsList(boxPayload, builder);\n      } else if (boxType == TYPE_payl) {\n        WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList());\n      } else {\n        // Other VTTCueBox children are still not supported and are ignored.\n      }\n    }\n    return builder.build();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Representation of a Webvtt subtitle embedded in a MP4 container file.\n */\n/* package */ final class Mp4WebvttSubtitle implements Subtitle {\n\n  private final List<Cue> cues;\n\n  public Mp4WebvttSubtitle(List<Cue> cueList) {\n    cues = Collections.unmodifiableList(cueList);\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    return timeUs < 0 ? 0 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return 1;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index == 0);\n    return 0;\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    return timeUs >= 0 ? cues : Collections.emptyList();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport android.graphics.Typeface;\nimport android.text.Layout;\nimport android.text.TextUtils;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\n\n/**\n * Style object of a Css style block in a Webvtt file.\n *\n * @see <a href=\"https://w3c.github.io/webvtt/#applying-css-properties\">W3C specification - Apply\n *     CSS properties</a>\n */\npublic final class WebvttCssStyle {\n\n  public static final int UNSPECIFIED = -1;\n\n  /**\n   * Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link\n   * #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC})\n  public @interface StyleFlags {}\n\n  public static final int STYLE_NORMAL = Typeface.NORMAL;\n  public static final int STYLE_BOLD = Typeface.BOLD;\n  public static final int STYLE_ITALIC = Typeface.ITALIC;\n  public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;\n\n  /**\n   * Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link\n   * #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})\n  public @interface FontSizeUnit {}\n\n  public static final int FONT_SIZE_UNIT_PIXEL = 1;\n  public static final int FONT_SIZE_UNIT_EM = 2;\n  public static final int FONT_SIZE_UNIT_PERCENT = 3;\n\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({UNSPECIFIED, OFF, ON})\n  private @interface OptionalBoolean {}\n\n  private static final int OFF = 0;\n  private static final int ON = 1;\n\n  // Selector properties.\n  private String targetId;\n  private String targetTag;\n  private List<String> targetClasses;\n  private String targetVoice;\n\n  // Style properties.\n  @Nullable private String fontFamily;\n  private int fontColor;\n  private boolean hasFontColor;\n  private int backgroundColor;\n  private boolean hasBackgroundColor;\n  @OptionalBoolean private int linethrough;\n  @OptionalBoolean private int underline;\n  @OptionalBoolean private int bold;\n  @OptionalBoolean private int italic;\n  @FontSizeUnit private int fontSizeUnit;\n  private float fontSize;\n  @Nullable private Layout.Alignment textAlign;\n\n  // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed\n  // because reset() only assigns fields, it doesn't read any.\n  @SuppressWarnings(\"nullness:method.invocation.invalid\")\n  public WebvttCssStyle() {\n    reset();\n  }\n\n  @EnsuresNonNull({\"targetId\", \"targetTag\", \"targetClasses\", \"targetVoice\"})\n  public void reset() {\n    targetId = \"\";\n    targetTag = \"\";\n    targetClasses = Collections.emptyList();\n    targetVoice = \"\";\n    fontFamily = null;\n    hasFontColor = false;\n    hasBackgroundColor = false;\n    linethrough = UNSPECIFIED;\n    underline = UNSPECIFIED;\n    bold = UNSPECIFIED;\n    italic = UNSPECIFIED;\n    fontSizeUnit = UNSPECIFIED;\n    textAlign = null;\n  }\n\n  public void setTargetId(String targetId) {\n    this.targetId  = targetId;\n  }\n\n  public void setTargetTagName(String targetTag) {\n    this.targetTag = targetTag;\n  }\n\n  public void setTargetClasses(String[] targetClasses) {\n    this.targetClasses = Arrays.asList(targetClasses);\n  }\n\n  public void setTargetVoice(String targetVoice) {\n    this.targetVoice = targetVoice;\n  }\n\n  /**\n   * Returns a value in a score system compliant with the CSS Specificity rules.\n   *\n   * @see <a href=\"https://www.w3.org/TR/CSS2/cascade.html\">CSS Cascading</a>\n   *     <p>The score works as follows:\n   *     <ul>\n   *       <li>Id match adds 0x40000000 to the score.\n   *       <li>Each class and voice match adds 4 to the score.\n   *       <li>Tag matching adds 2 to the score.\n   *       <li>Universal selector matching scores 1.\n   *     </ul>\n   *\n   * @param id The id of the cue if present, {@code null} otherwise.\n   * @param tag Name of the tag, {@code null} if it refers to the entire cue.\n   * @param classes An array containing the classes the tag belongs to. Must not be null.\n   * @param voice Annotated voice if present, {@code null} otherwise.\n   * @return The score of the match, zero if there is no match.\n   */\n  public int getSpecificityScore(\n      @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) {\n    if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()\n        && targetVoice.isEmpty()) {\n      // The selector is universal. It matches with the minimum score if and only if the given\n      // element is a whole cue.\n      return TextUtils.isEmpty(tag) ? 1 : 0;\n    }\n    int score = 0;\n    score = updateScoreForMatch(score, targetId, id, 0x40000000);\n    score = updateScoreForMatch(score, targetTag, tag, 2);\n    score = updateScoreForMatch(score, targetVoice, voice, 4);\n    if (score == -1 || !Arrays.asList(classes).containsAll(targetClasses)) {\n      return 0;\n    } else {\n      score += targetClasses.size() * 4;\n    }\n    return score;\n  }\n\n  /**\n   * Returns the style or {@link #UNSPECIFIED} when no style information is given.\n   *\n   * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}\n   *     or {@link #STYLE_BOLD_ITALIC}.\n   */\n  @StyleFlags public int getStyle() {\n    if (bold == UNSPECIFIED && italic == UNSPECIFIED) {\n      return UNSPECIFIED;\n    }\n    return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)\n        | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);\n  }\n\n  public boolean isLinethrough() {\n    return linethrough == ON;\n  }\n\n  public WebvttCssStyle setLinethrough(boolean linethrough) {\n    this.linethrough = linethrough ? ON : OFF;\n    return this;\n  }\n\n  public boolean isUnderline() {\n    return underline == ON;\n  }\n\n  public WebvttCssStyle setUnderline(boolean underline) {\n    this.underline = underline ? ON : OFF;\n    return this;\n  }\n  public WebvttCssStyle setBold(boolean bold) {\n    this.bold = bold ? ON : OFF;\n    return this;\n  }\n\n  public WebvttCssStyle setItalic(boolean italic) {\n    this.italic = italic ? ON : OFF;\n    return this;\n  }\n\n  @Nullable\n  public String getFontFamily() {\n    return fontFamily;\n  }\n\n  public WebvttCssStyle setFontFamily(@Nullable String fontFamily) {\n    this.fontFamily = Util.toLowerInvariant(fontFamily);\n    return this;\n  }\n\n  public int getFontColor() {\n    if (!hasFontColor) {\n      throw new IllegalStateException(\"Font color not defined\");\n    }\n    return fontColor;\n  }\n\n  public WebvttCssStyle setFontColor(int color) {\n    this.fontColor = color;\n    hasFontColor = true;\n    return this;\n  }\n\n  public boolean hasFontColor() {\n    return hasFontColor;\n  }\n\n  public int getBackgroundColor() {\n    if (!hasBackgroundColor) {\n      throw new IllegalStateException(\"Background color not defined.\");\n    }\n    return backgroundColor;\n  }\n\n  public WebvttCssStyle setBackgroundColor(int backgroundColor) {\n    this.backgroundColor = backgroundColor;\n    hasBackgroundColor = true;\n    return this;\n  }\n\n  public boolean hasBackgroundColor() {\n    return hasBackgroundColor;\n  }\n\n  @Nullable\n  public Layout.Alignment getTextAlign() {\n    return textAlign;\n  }\n\n  public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) {\n    this.textAlign = textAlign;\n    return this;\n  }\n\n  public WebvttCssStyle setFontSize(float fontSize) {\n    this.fontSize = fontSize;\n    return this;\n  }\n\n  public WebvttCssStyle setFontSizeUnit(short unit) {\n    this.fontSizeUnit = unit;\n    return this;\n  }\n\n  @FontSizeUnit public int getFontSizeUnit() {\n    return fontSizeUnit;\n  }\n\n  public float getFontSize() {\n    return fontSize;\n  }\n\n  public void cascadeFrom(WebvttCssStyle style) {\n    if (style.hasFontColor) {\n      setFontColor(style.fontColor);\n    }\n    if (style.bold != UNSPECIFIED) {\n      bold = style.bold;\n    }\n    if (style.italic != UNSPECIFIED) {\n      italic = style.italic;\n    }\n    if (style.fontFamily != null) {\n      fontFamily = style.fontFamily;\n    }\n    if (linethrough == UNSPECIFIED) {\n      linethrough = style.linethrough;\n    }\n    if (underline == UNSPECIFIED) {\n      underline = style.underline;\n    }\n    if (textAlign == null) {\n      textAlign = style.textAlign;\n    }\n    if (fontSizeUnit == UNSPECIFIED) {\n      fontSizeUnit = style.fontSizeUnit;\n      fontSize = style.fontSize;\n    }\n    if (style.hasBackgroundColor) {\n      setBackgroundColor(style.backgroundColor);\n    }\n  }\n\n  private static int updateScoreForMatch(\n      int currentScore, String target, @Nullable String actual, int score) {\n    if (target.isEmpty() || currentScore == -1) {\n      return currentScore;\n    }\n    return target.equals(actual) ? currentScore + score : -1;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport static java.lang.annotation.RetentionPolicy.SOURCE;\n\nimport android.text.Layout.Alignment;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\n\n/** A representation of a WebVTT cue. */\npublic final class WebvttCue extends Cue {\n\n  private static final float DEFAULT_POSITION = 0.5f;\n\n  public final long startTime;\n  public final long endTime;\n\n  private WebvttCue(\n      long startTime,\n      long endTime,\n      CharSequence text,\n      @Nullable Alignment textAlignment,\n      float line,\n      @Cue.LineType int lineType,\n      @Cue.AnchorType int lineAnchor,\n      float position,\n      @Cue.AnchorType int positionAnchor,\n      float width) {\n    super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);\n    this.startTime = startTime;\n    this.endTime = endTime;\n  }\n\n  /**\n   * Returns whether or not this cue should be placed in the default position and rolled-up with\n   * the other \"normal\" cues.\n   *\n   * @return Whether this cue should be placed in the default position.\n   */\n  public boolean isNormalCue() {\n    return (line == DIMEN_UNSET && position == DEFAULT_POSITION);\n  }\n\n  /** Builder for WebVTT cues. */\n  @SuppressWarnings(\"hiding\")\n  public static class Builder {\n\n    /**\n     * Valid values for {@link #setTextAlignment(int)}.\n     *\n     * <p>We use a custom list (and not {@link Alignment} directly) in order to include both {@code\n     * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link\n     * #derivePosition(int)}.\n     *\n     * <p>These correspond to the valid values for the 'align' cue setting in the <a\n     * href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment\">WebVTT spec</a>.\n     */\n    @Documented\n    @Retention(SOURCE)\n    @IntDef({\n        TEXT_ALIGNMENT_START,\n        TEXT_ALIGNMENT_CENTER,\n        TEXT_ALIGNMENT_END,\n        TEXT_ALIGNMENT_LEFT,\n        TEXT_ALIGNMENT_RIGHT\n    })\n    public @interface TextAlignment {}\n    /**\n     * See WebVTT's <a\n     * href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-start-alignment\">align:start</a>.\n     */\n    public static final int TEXT_ALIGNMENT_START = 1;\n\n    /**\n     * See WebVTT's <a\n     * href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-center-alignment\">align:center</a>.\n     */\n    public static final int TEXT_ALIGNMENT_CENTER = 2;\n\n    /**\n     * See WebVTT's <a href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-end-alignment\">align:end</a>.\n     */\n    public static final int TEXT_ALIGNMENT_END = 3;\n\n    /**\n     * See WebVTT's <a href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-left-alignment\">align:left</a>.\n     */\n    public static final int TEXT_ALIGNMENT_LEFT = 4;\n\n    /**\n     * See WebVTT's <a\n     * href=\"https://www.w3.org/TR/webvtt1/#webvtt-cue-right-alignment\">align:right</a>.\n     */\n    public static final int TEXT_ALIGNMENT_RIGHT = 5;\n\n    private static final String TAG = \"WebvttCueBuilder\";\n\n    private long startTime;\n    private long endTime;\n    @Nullable private CharSequence text;\n    @TextAlignment private int textAlignment;\n    private float line;\n    // Equivalent to WebVTT's snap-to-lines flag:\n    // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag\n    @LineType private int lineType;\n    @AnchorType private int lineAnchor;\n    private float position;\n    @AnchorType private int positionAnchor;\n    private float width;\n\n    // Initialization methods\n\n    // Calling reset() is forbidden because `this` isn't initialized. This can be safely\n    // suppressed because reset() only assigns fields, it doesn't read any.\n    @SuppressWarnings(\"nullness:method.invocation.invalid\")\n    public Builder() {\n      reset();\n    }\n\n    public void reset() {\n      startTime = 0;\n      endTime = 0;\n      text = null;\n      // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment\n      textAlignment = TEXT_ALIGNMENT_CENTER;\n      line = Cue.DIMEN_UNSET;\n      // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag\n      lineType = Cue.LINE_TYPE_NUMBER;\n      // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment\n      lineAnchor = Cue.ANCHOR_TYPE_START;\n      position = Cue.DIMEN_UNSET;\n      positionAnchor = Cue.TYPE_UNSET;\n      // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size\n      width = 1.0f;\n    }\n\n    // Construction methods.\n\n    public WebvttCue build() {\n      line = computeLine(line, lineType);\n\n      if (position == Cue.DIMEN_UNSET) {\n        position = derivePosition(textAlignment);\n      }\n\n      if (positionAnchor == Cue.TYPE_UNSET) {\n        positionAnchor = derivePositionAnchor(textAlignment);\n      }\n\n      width = Math.min(width, deriveMaxSize(positionAnchor, position));\n\n      return new WebvttCue(\n          startTime,\n          endTime,\n          Assertions.checkNotNull(text),\n          convertTextAlignment(textAlignment),\n          line,\n          lineType,\n          lineAnchor,\n          position,\n          positionAnchor,\n          width);\n    }\n\n    public Builder setStartTime(long time) {\n      startTime = time;\n      return this;\n    }\n\n    public Builder setEndTime(long time) {\n      endTime = time;\n      return this;\n    }\n\n    public Builder setText(CharSequence text) {\n      this.text = text;\n      return this;\n    }\n\n    public Builder setTextAlignment(@TextAlignment int textAlignment) {\n      this.textAlignment = textAlignment;\n      return this;\n    }\n\n    public Builder setLine(float line) {\n      this.line = line;\n      return this;\n    }\n\n    public Builder setLineType(@LineType int lineType) {\n      this.lineType = lineType;\n      return this;\n    }\n\n    public Builder setLineAnchor(@AnchorType int lineAnchor) {\n      this.lineAnchor = lineAnchor;\n      return this;\n    }\n\n    public Builder setPosition(float position) {\n      this.position = position;\n      return this;\n    }\n\n    public Builder setPositionAnchor(@AnchorType int positionAnchor) {\n      this.positionAnchor = positionAnchor;\n      return this;\n    }\n\n    public Builder setWidth(float width) {\n      this.width = width;\n      return this;\n    }\n\n    // https://www.w3.org/TR/webvtt1/#webvtt-cue-line\n    private static float computeLine(float line, @LineType int lineType) {\n      if (line != Cue.DIMEN_UNSET\n          && lineType == Cue.LINE_TYPE_FRACTION\n          && (line < 0.0f || line > 1.0f)) {\n        return 1.0f; // Step 1\n      } else if (line != Cue.DIMEN_UNSET) {\n        // Step 2: Do nothing, line is already correct.\n        return line;\n      } else if (lineType == Cue.LINE_TYPE_FRACTION) {\n        return 1.0f; // Step 3\n      } else {\n        // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues\n        // and WebvttCue#isNormalCue.\n        return DIMEN_UNSET;\n      }\n    }\n\n    // https://www.w3.org/TR/webvtt1/#webvtt-cue-position\n    private static float derivePosition(@TextAlignment int textAlignment) {\n      switch (textAlignment) {\n        case TEXT_ALIGNMENT_LEFT:\n          return 0.0f;\n        case TEXT_ALIGNMENT_RIGHT:\n          return 1.0f;\n        case TEXT_ALIGNMENT_START:\n        case TEXT_ALIGNMENT_CENTER:\n        case TEXT_ALIGNMENT_END:\n        default:\n          return DEFAULT_POSITION;\n      }\n    }\n\n    // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment\n    @AnchorType\n    private static int derivePositionAnchor(@TextAlignment int textAlignment) {\n      switch (textAlignment) {\n        case TEXT_ALIGNMENT_LEFT:\n        case TEXT_ALIGNMENT_START:\n          return Cue.ANCHOR_TYPE_START;\n        case TEXT_ALIGNMENT_RIGHT:\n        case TEXT_ALIGNMENT_END:\n          return Cue.ANCHOR_TYPE_END;\n        case TEXT_ALIGNMENT_CENTER:\n        default:\n          return Cue.ANCHOR_TYPE_MIDDLE;\n      }\n    }\n\n    @Nullable\n    private static Alignment convertTextAlignment(@TextAlignment int textAlignment) {\n      switch (textAlignment) {\n        case TEXT_ALIGNMENT_START:\n        case TEXT_ALIGNMENT_LEFT:\n          return Alignment.ALIGN_NORMAL;\n        case TEXT_ALIGNMENT_CENTER:\n          return Alignment.ALIGN_CENTER;\n        case TEXT_ALIGNMENT_END:\n        case TEXT_ALIGNMENT_RIGHT:\n          return Alignment.ALIGN_OPPOSITE;\n        default:\n          Log.w(TAG, \"Unknown textAlignment: \" + textAlignment);\n          return null;\n      }\n    }\n\n    // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings\n    private static float deriveMaxSize(@AnchorType int positionAnchor, float position) {\n      switch (positionAnchor) {\n        case Cue.ANCHOR_TYPE_START:\n          return 1.0f - position;\n        case Cue.ANCHOR_TYPE_END:\n          return position;\n        case Cue.ANCHOR_TYPE_MIDDLE:\n          if (position <= 0.5f) {\n            return position * 2;\n          } else {\n            return (1.0f - position) * 2;\n          }\n        case Cue.TYPE_UNSET:\n        default:\n          throw new IllegalStateException(String.valueOf(positionAnchor));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport android.graphics.Typeface;\nimport android.text.Layout;\nimport android.text.Spannable;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.AlignmentSpan;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.text.style.StrikethroughSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */\npublic final class WebvttCueParser {\n\n  public static final Pattern CUE_HEADER_PATTERN = Pattern\n      .compile(\"^(\\\\S+)\\\\s+-->\\\\s+(\\\\S+)(.*)?$\");\n\n  private static final Pattern CUE_SETTING_PATTERN = Pattern.compile(\"(\\\\S+?):(\\\\S+)\");\n\n  private static final char CHAR_LESS_THAN = '<';\n  private static final char CHAR_GREATER_THAN = '>';\n  private static final char CHAR_SLASH = '/';\n  private static final char CHAR_AMPERSAND = '&';\n  private static final char CHAR_SEMI_COLON = ';';\n  private static final char CHAR_SPACE = ' ';\n\n  private static final String ENTITY_LESS_THAN = \"lt\";\n  private static final String ENTITY_GREATER_THAN = \"gt\";\n  private static final String ENTITY_AMPERSAND = \"amp\";\n  private static final String ENTITY_NON_BREAK_SPACE = \"nbsp\";\n\n  private static final String TAG_BOLD = \"b\";\n  private static final String TAG_ITALIC = \"i\";\n  private static final String TAG_UNDERLINE = \"u\";\n  private static final String TAG_CLASS = \"c\";\n  private static final String TAG_VOICE = \"v\";\n  private static final String TAG_LANG = \"lang\";\n\n  private static final int STYLE_BOLD = Typeface.BOLD;\n  private static final int STYLE_ITALIC = Typeface.ITALIC;\n\n  private static final String TAG = \"WebvttCueParser\";\n\n  private final StringBuilder textBuilder;\n\n  public WebvttCueParser() {\n    textBuilder = new StringBuilder();\n  }\n\n  /**\n   * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.\n   *\n   * @param webvttData Parsable WebVTT file data.\n   * @param builder Builder for WebVTT Cues (output parameter).\n   * @param styles List of styles defined by the CSS style blocks preceding the cues.\n   * @return Whether a valid Cue was found.\n   */\n  public boolean parseCue(\n      ParsableByteArray webvttData, WebvttCue.Builder builder, List<WebvttCssStyle> styles) {\n    @Nullable String firstLine = webvttData.readLine();\n    if (firstLine == null) {\n      return false;\n    }\n    Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);\n    if (cueHeaderMatcher.matches()) {\n      // We have found the timestamps in the first line. No id present.\n      return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);\n    }\n    // The first line is not the timestamps, but could be the cue id.\n    @Nullable String secondLine = webvttData.readLine();\n    if (secondLine == null) {\n      return false;\n    }\n    cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);\n    if (cueHeaderMatcher.matches()) {\n      // We can do the rest of the parsing, including the id.\n      return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,\n          styles);\n    }\n    return false;\n  }\n\n  /**\n   * Parses a string containing a list of cue settings.\n   *\n   * @param cueSettingsList String containing the settings for a given cue.\n   * @param builder The {@link WebvttCue.Builder} where incremental construction takes place.\n   */\n  /* package */ static void parseCueSettingsList(String cueSettingsList,\n      WebvttCue.Builder builder) {\n    // Parse the cue settings list.\n    Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);\n    while (cueSettingMatcher.find()) {\n      String name = cueSettingMatcher.group(1);\n      String value = cueSettingMatcher.group(2);\n      try {\n        if (\"line\".equals(name)) {\n          parseLineAttribute(value, builder);\n        } else if (\"align\".equals(name)) {\n          builder.setTextAlignment(parseTextAlignment(value));\n        } else if (\"position\".equals(name)) {\n          parsePositionAttribute(value, builder);\n        } else if (\"size\".equals(name)) {\n          builder.setWidth(WebvttParserUtil.parsePercentage(value));\n        } else {\n          Log.w(TAG, \"Unknown cue setting \" + name + \":\" + value);\n        }\n      } catch (NumberFormatException e) {\n        Log.w(TAG, \"Skipping bad cue setting: \" + cueSettingMatcher.group());\n      }\n    }\n  }\n\n  /**\n   * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.\n   *\n   * @param id Id of the cue, {@code null} if it is not present.\n   * @param markup The markup text to be parsed.\n   * @param styles List of styles defined by the CSS style blocks preceding the cues.\n   * @param builder Output builder.\n   */\n  /* package */ static void parseCueText(\n      @Nullable String id, String markup, WebvttCue.Builder builder, List<WebvttCssStyle> styles) {\n    SpannableStringBuilder spannedText = new SpannableStringBuilder();\n    ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();\n    List<StyleMatch> scratchStyleMatches = new ArrayList<>();\n    int pos = 0;\n    while (pos < markup.length()) {\n      char curr = markup.charAt(pos);\n      switch (curr) {\n        case CHAR_LESS_THAN:\n          if (pos + 1 >= markup.length()) {\n            pos++;\n            break; // avoid ArrayOutOfBoundsException\n          }\n          int ltPos = pos;\n          boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;\n          pos = findEndOfTag(markup, ltPos + 1);\n          boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH;\n          String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1),\n              isVoidTag ? pos - 2 : pos - 1);\n          if (fullTagExpression.trim().isEmpty()) {\n            continue;\n          }\n          String tagName = getTagName(fullTagExpression);\n          if (!isSupportedTag(tagName)) {\n            continue;\n          }\n          if (isClosingTag) {\n            StartTag startTag;\n            do {\n              if (startTagStack.isEmpty()) {\n                break;\n              }\n              startTag = startTagStack.pop();\n              applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);\n            } while(!startTag.name.equals(tagName));\n          } else if (!isVoidTag) {\n            startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));\n          }\n          break;\n        case CHAR_AMPERSAND:\n          int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1);\n          int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1);\n          int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex\n              : (spaceEndIndex == -1 ? semiColonEndIndex\n                  : Math.min(semiColonEndIndex, spaceEndIndex));\n          if (entityEndIndex != -1) {\n            applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText);\n            if (entityEndIndex == spaceEndIndex) {\n              spannedText.append(\" \");\n            }\n            pos = entityEndIndex + 1;\n          } else {\n            spannedText.append(curr);\n            pos++;\n          }\n          break;\n        default:\n          spannedText.append(curr);\n          pos++;\n          break;\n      }\n    }\n    // apply unclosed tags\n    while (!startTagStack.isEmpty()) {\n      applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);\n    }\n    applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,\n        scratchStyleMatches);\n    builder.setText(spannedText);\n  }\n\n  private static boolean parseCue(\n      @Nullable String id,\n      Matcher cueHeaderMatcher,\n      ParsableByteArray webvttData,\n      WebvttCue.Builder builder,\n      StringBuilder textBuilder,\n      List<WebvttCssStyle> styles) {\n    try {\n      // Parse the cue start and end times.\n      builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))\n          .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));\n    } catch (NumberFormatException e) {\n      Log.w(TAG, \"Skipping cue with bad header: \" + cueHeaderMatcher.group());\n      return false;\n    }\n\n    parseCueSettingsList(cueHeaderMatcher.group(3), builder);\n\n    // Parse the cue text.\n    textBuilder.setLength(0);\n    for (String line = webvttData.readLine();\n        !TextUtils.isEmpty(line);\n        line = webvttData.readLine()) {\n      if (textBuilder.length() > 0) {\n        textBuilder.append(\"\\n\");\n      }\n      textBuilder.append(line.trim());\n    }\n    parseCueText(id, textBuilder.toString(), builder, styles);\n    return true;\n  }\n\n  // Internal methods\n\n  private static void parseLineAttribute(String s, WebvttCue.Builder builder) {\n    int commaIndex = s.indexOf(',');\n    if (commaIndex != -1) {\n      builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n      s = s.substring(0, commaIndex);\n    }\n    if (s.endsWith(\"%\")) {\n      builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);\n    } else {\n      int lineNumber = Integer.parseInt(s);\n      if (lineNumber < 0) {\n        // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as\n        // Cue defines it to be the first row that's not visible.\n        lineNumber--;\n      }\n      builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);\n    }\n  }\n\n  private static void parsePositionAttribute(String s, WebvttCue.Builder builder) {\n    int commaIndex = s.indexOf(',');\n    if (commaIndex != -1) {\n      builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n      s = s.substring(0, commaIndex);\n    }\n    builder.setPosition(WebvttParserUtil.parsePercentage(s));\n  }\n\n  @Cue.AnchorType\n  private static int parsePositionAnchor(String s) {\n    switch (s) {\n      case \"start\":\n        return Cue.ANCHOR_TYPE_START;\n      case \"center\":\n      case \"middle\":\n        return Cue.ANCHOR_TYPE_MIDDLE;\n      case \"end\":\n        return Cue.ANCHOR_TYPE_END;\n      default:\n        Log.w(TAG, \"Invalid anchor value: \" + s);\n        return Cue.TYPE_UNSET;\n    }\n  }\n\n  @WebvttCue.Builder.TextAlignment\n  private static int parseTextAlignment(String s) {\n    switch (s) {\n      case \"start\":\n        return WebvttCue.Builder.TEXT_ALIGNMENT_START;\n      case \"left\":\n        return WebvttCue.Builder.TEXT_ALIGNMENT_LEFT;\n      case \"center\":\n      case \"middle\":\n        return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;\n      case \"end\":\n        return WebvttCue.Builder.TEXT_ALIGNMENT_END;\n      case \"right\":\n        return WebvttCue.Builder.TEXT_ALIGNMENT_RIGHT;\n      default:\n        Log.w(TAG, \"Invalid alignment value: \" + s);\n        // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment\n        return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;\n    }\n  }\n\n  /**\n   * Find end of tag (&gt;). The position returned is the position of the &gt; plus one (exclusive).\n   *\n   * @param markup The WebVTT cue markup to be parsed.\n   * @param startPos The position from where to start searching for the end of tag.\n   * @return The position of the end of tag plus 1 (one).\n   */\n  private static int findEndOfTag(String markup, int startPos) {\n    int index = markup.indexOf(CHAR_GREATER_THAN, startPos);\n    return index == -1 ? markup.length() : index + 1;\n  }\n\n  private static void applyEntity(String entity, SpannableStringBuilder spannedText) {\n    switch (entity) {\n      case ENTITY_LESS_THAN:\n        spannedText.append('<');\n        break;\n      case ENTITY_GREATER_THAN:\n        spannedText.append('>');\n        break;\n      case ENTITY_NON_BREAK_SPACE:\n        spannedText.append(' ');\n        break;\n      case ENTITY_AMPERSAND:\n        spannedText.append('&');\n        break;\n      default:\n        Log.w(TAG, \"ignoring unsupported entity: '&\" + entity + \";'\");\n        break;\n    }\n  }\n\n  private static boolean isSupportedTag(String tagName) {\n    switch (tagName) {\n      case TAG_BOLD:\n      case TAG_CLASS:\n      case TAG_ITALIC:\n      case TAG_LANG:\n      case TAG_UNDERLINE:\n      case TAG_VOICE:\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  private static void applySpansForTag(\n      @Nullable String cueId,\n      StartTag startTag,\n      SpannableStringBuilder text,\n      List<WebvttCssStyle> styles,\n      List<StyleMatch> scratchStyleMatches) {\n    int start = startTag.position;\n    int end = text.length();\n    switch(startTag.name) {\n      case TAG_BOLD:\n        text.setSpan(new StyleSpan(STYLE_BOLD), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TAG_ITALIC:\n        text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TAG_UNDERLINE:\n        text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case TAG_CLASS:\n      case TAG_LANG:\n      case TAG_VOICE:\n      case \"\": // Case of the \"whole cue\" virtual tag.\n        break;\n      default:\n        return;\n    }\n    scratchStyleMatches.clear();\n    getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);\n    int styleMatchesCount = scratchStyleMatches.size();\n    for (int i = 0; i < styleMatchesCount; i++) {\n      applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);\n    }\n  }\n\n  private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,\n      int start, int end) {\n    if (style == null) {\n      return;\n    }\n    if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {\n      spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.isLinethrough()) {\n      spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.isUnderline()) {\n      spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.hasFontColor()) {\n      spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,\n          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.hasBackgroundColor()) {\n      spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,\n          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    if (style.getFontFamily() != null) {\n      spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,\n          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    Layout.Alignment textAlign = style.getTextAlign();\n    if (textAlign != null) {\n      spannedText.setSpan(\n          new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n    }\n    switch (style.getFontSizeUnit()) {\n      case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:\n        spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case WebvttCssStyle.FONT_SIZE_UNIT_EM:\n        spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:\n        spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,\n            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        break;\n      case WebvttCssStyle.UNSPECIFIED:\n        // Do nothing.\n        break;\n    }\n  }\n\n  /**\n   * Returns the tag name for the given tag contents.\n   *\n   * @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag.\n   * @return The name of tag.\n   */\n  private static String getTagName(String tagExpression) {\n    tagExpression = tagExpression.trim();\n    Assertions.checkArgument(!tagExpression.isEmpty());\n    return Util.splitAtFirst(tagExpression, \"[ \\\\.]\")[0];\n  }\n\n  private static void getApplicableStyles(\n      List<WebvttCssStyle> declaredStyles,\n      @Nullable String id,\n      StartTag tag,\n      List<StyleMatch> output) {\n    int styleCount = declaredStyles.size();\n    for (int i = 0; i < styleCount; i++) {\n      WebvttCssStyle style = declaredStyles.get(i);\n      int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);\n      if (score > 0) {\n        output.add(new StyleMatch(score, style));\n      }\n    }\n    Collections.sort(output);\n  }\n\n  private static final class StyleMatch implements Comparable<StyleMatch> {\n\n    public final int score;\n    public final WebvttCssStyle style;\n\n    public StyleMatch(int score, WebvttCssStyle style) {\n      this.score = score;\n      this.style = style;\n    }\n\n    @Override\n    public int compareTo(@NonNull StyleMatch another) {\n      return this.score - another.score;\n    }\n\n  }\n\n  private static final class StartTag {\n\n    private static final String[] NO_CLASSES = new String[0];\n\n    public final String name;\n    public final int position;\n    public final String voice;\n    public final String[] classes;\n\n    private StartTag(String name, int position, String voice, String[] classes) {\n      this.position = position;\n      this.name = name;\n      this.voice = voice;\n      this.classes = classes;\n    }\n\n    public static StartTag buildStartTag(String fullTagExpression, int position) {\n      fullTagExpression = fullTagExpression.trim();\n      Assertions.checkArgument(!fullTagExpression.isEmpty());\n      int voiceStartIndex = fullTagExpression.indexOf(\" \");\n      String voice;\n      if (voiceStartIndex == -1) {\n        voice = \"\";\n      } else {\n        voice = fullTagExpression.substring(voiceStartIndex).trim();\n        fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);\n      }\n      String[] nameAndClasses = Util.split(fullTagExpression, \"\\\\.\");\n      String name = nameAndClasses[0];\n      String[] classes;\n      if (nameAndClasses.length > 1) {\n        classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length);\n      } else {\n        classes = NO_CLASSES;\n      }\n      return new StartTag(name, position, voice, classes);\n    }\n\n    public static StartTag buildWholeCueVirtualTag() {\n      return new StartTag(\"\", 0, \"\", new String[0]);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A {@link SimpleSubtitleDecoder} for WebVTT.\n * <p>\n * @see <a href=\"http://dev.w3.org/html5/webvtt\">WebVTT specification</a>\n */\npublic final class WebvttDecoder extends SimpleSubtitleDecoder {\n\n  private static final int EVENT_NONE = -1;\n  private static final int EVENT_END_OF_FILE = 0;\n  private static final int EVENT_COMMENT = 1;\n  private static final int EVENT_STYLE_BLOCK = 2;\n  private static final int EVENT_CUE = 3;\n\n  private static final String COMMENT_START = \"NOTE\";\n  private static final String STYLE_START = \"STYLE\";\n\n  private final WebvttCueParser cueParser;\n  private final ParsableByteArray parsableWebvttData;\n  private final WebvttCue.Builder webvttCueBuilder;\n  private final CssParser cssParser;\n  private final List<WebvttCssStyle> definedStyles;\n\n  public WebvttDecoder() {\n    super(\"WebvttDecoder\");\n    cueParser = new WebvttCueParser();\n    parsableWebvttData = new ParsableByteArray();\n    webvttCueBuilder = new WebvttCue.Builder();\n    cssParser = new CssParser();\n    definedStyles = new ArrayList<>();\n  }\n\n  @Override\n  protected Subtitle decode(byte[] bytes, int length, boolean reset)\n      throws SubtitleDecoderException {\n    parsableWebvttData.reset(bytes, length);\n    // Initialization for consistent starting state.\n    webvttCueBuilder.reset();\n    definedStyles.clear();\n\n    // Validate the first line of the header, and skip the remainder.\n    try {\n      WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);\n    } catch (ParserException e) {\n      throw new SubtitleDecoderException(e);\n    }\n    while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}\n\n    int event;\n    ArrayList<WebvttCue> subtitles = new ArrayList<>();\n    while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {\n      if (event == EVENT_COMMENT) {\n        skipComment(parsableWebvttData);\n      } else if (event == EVENT_STYLE_BLOCK) {\n        if (!subtitles.isEmpty()) {\n          throw new SubtitleDecoderException(\"A style block was found after the first cue.\");\n        }\n        parsableWebvttData.readLine(); // Consume the \"STYLE\" header.\n        definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));\n      } else if (event == EVENT_CUE) {\n        if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {\n          subtitles.add(webvttCueBuilder.build());\n          webvttCueBuilder.reset();\n        }\n      }\n    }\n    return new WebvttSubtitle(subtitles);\n  }\n\n  /**\n   * Positions the input right before the next event, and returns the kind of event found. Does not\n   * consume any data from such event, if any.\n   *\n   * @return The kind of event found.\n   */\n  private static int getNextEvent(ParsableByteArray parsableWebvttData) {\n    int foundEvent = EVENT_NONE;\n    int currentInputPosition = 0;\n    while (foundEvent == EVENT_NONE) {\n      currentInputPosition = parsableWebvttData.getPosition();\n      String line = parsableWebvttData.readLine();\n      if (line == null) {\n        foundEvent = EVENT_END_OF_FILE;\n      } else if (STYLE_START.equals(line)) {\n        foundEvent = EVENT_STYLE_BLOCK;\n      } else if (line.startsWith(COMMENT_START)) {\n        foundEvent = EVENT_COMMENT;\n      } else {\n        foundEvent = EVENT_CUE;\n      }\n    }\n    parsableWebvttData.setPosition(currentInputPosition);\n    return foundEvent;\n  }\n\n  private static void skipComment(ParsableByteArray parsableWebvttData) {\n    while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Utility methods for parsing WebVTT data.\n */\npublic final class WebvttParserUtil {\n\n  private static final Pattern COMMENT = Pattern.compile(\"^NOTE([ \\t].*)?$\");\n  private static final String WEBVTT_HEADER = \"WEBVTT\";\n\n  private WebvttParserUtil() {}\n\n  /**\n   * Reads and validates the first line of a WebVTT file.\n   *\n   * @param input The input from which the line should be read.\n   * @throws ParserException If the line isn't the start of a valid WebVTT file.\n   */\n  public static void validateWebvttHeaderLine(ParsableByteArray input) throws ParserException {\n    int startPosition = input.getPosition();\n    if (!isWebvttHeaderLine(input)) {\n      input.setPosition(startPosition);\n      throw new ParserException(\"Expected WEBVTT. Got \" + input.readLine());\n    }\n  }\n\n  /**\n   * Returns whether the given input is the first line of a WebVTT file.\n   *\n   * @param input The input from which the line should be read.\n   */\n  public static boolean isWebvttHeaderLine(ParsableByteArray input) {\n    @Nullable String line = input.readLine();\n    return line != null && line.startsWith(WEBVTT_HEADER);\n  }\n\n  /**\n   * Parses a WebVTT timestamp.\n   *\n   * @param timestamp The timestamp string.\n   * @return The parsed timestamp in microseconds.\n   * @throws NumberFormatException If the timestamp could not be parsed.\n   */\n  public static long parseTimestampUs(String timestamp) throws NumberFormatException {\n    long value = 0;\n    String[] parts = Util.splitAtFirst(timestamp, \"\\\\.\");\n    String[] subparts = Util.split(parts[0], \":\");\n    for (String subpart : subparts) {\n      value = (value * 60) + Long.parseLong(subpart);\n    }\n    value *= 1000;\n    if (parts.length == 2) {\n      value += Long.parseLong(parts[1]);\n    }\n    return value * 1000;\n  }\n\n  /**\n   * Parses a percentage string.\n   *\n   * @param s The percentage string.\n   * @return The parsed value, where 1.0 represents 100%.\n   * @throws NumberFormatException If the percentage could not be parsed.\n   */\n  public static float parsePercentage(String s) throws NumberFormatException {\n    if (!s.endsWith(\"%\")) {\n      throw new NumberFormatException(\"Percentages must end with %\");\n    }\n    return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;\n  }\n\n  /**\n   * Reads lines up to and including the next WebVTT cue header.\n   *\n   * @param input The input from which lines should be read.\n   * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was\n   *     reached without a cue header being found. In the case that a cue header is found, groups 1,\n   *     2 and 3 of the returned matcher contain the start time, end time and settings list.\n   */\n  @Nullable\n  public static Matcher findNextCueHeader(ParsableByteArray input) {\n    @Nullable String line;\n    while ((line = input.readLine()) != null) {\n      if (COMMENT.matcher(line).matches()) {\n        // Skip until the end of the comment block.\n        while ((line = input.readLine()) != null && !line.isEmpty()) {}\n      } else {\n        Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);\n        if (cueHeaderMatcher.matches()) {\n          return cueHeaderMatcher;\n        }\n      }\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport android.text.SpannableStringBuilder;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * A representation of a WebVTT subtitle.\n */\n/* package */ final class WebvttSubtitle implements Subtitle {\n\n  private final List<WebvttCue> cues;\n  private final int numCues;\n  private final long[] cueTimesUs;\n  private final long[] sortedCueTimesUs;\n\n  /**\n   * @param cues A list of the cues in this subtitle.\n   */\n  public WebvttSubtitle(List<WebvttCue> cues) {\n    this.cues = cues;\n    numCues = cues.size();\n    cueTimesUs = new long[2 * numCues];\n    for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {\n      WebvttCue cue = cues.get(cueIndex);\n      int arrayIndex = cueIndex * 2;\n      cueTimesUs[arrayIndex] = cue.startTime;\n      cueTimesUs[arrayIndex + 1] = cue.endTime;\n    }\n    sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);\n    Arrays.sort(sortedCueTimesUs);\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);\n    return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return sortedCueTimesUs.length;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index >= 0);\n    Assertions.checkArgument(index < sortedCueTimesUs.length);\n    return sortedCueTimesUs[index];\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    List<Cue> list = new ArrayList<>();\n    WebvttCue firstNormalCue = null;\n    SpannableStringBuilder normalCueTextBuilder = null;\n\n    for (int i = 0; i < numCues; i++) {\n      if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {\n        WebvttCue cue = cues.get(i);\n        // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping\n        // individual cues, but tweaking their `line` value):\n        // https://www.w3.org/TR/webvtt1/#cue-computed-line\n        if (cue.isNormalCue()) {\n          // we want to merge all of the normal cues into a single cue to ensure they are drawn\n          // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple\n          // normal cues, otherwise we can just append the single normal cue\n          if (firstNormalCue == null) {\n            firstNormalCue = cue;\n          } else if (normalCueTextBuilder == null) {\n            normalCueTextBuilder = new SpannableStringBuilder();\n            normalCueTextBuilder\n                .append(Assertions.checkNotNull(firstNormalCue.text))\n                .append(\"\\n\")\n                .append(Assertions.checkNotNull(cue.text));\n          } else {\n            normalCueTextBuilder.append(\"\\n\").append(Assertions.checkNotNull(cue.text));\n          }\n        } else {\n          list.add(cue);\n        }\n      }\n    }\n    if (normalCueTextBuilder != null) {\n      // there were multiple normal cues, so create a new cue with all of the text\n      list.add(new WebvttCue.Builder().setText(normalCueTextBuilder).build());\n    } else if (firstNormalCue != null) {\n      // there was only a single normal cue, so just add it to the list\n      list.add(firstNormalCue);\n    }\n    return list;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.text.webvtt;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one\n * of highest quality given the current network conditions and the state of the buffer.\n */\npublic class AdaptiveTrackSelection extends BaseTrackSelection {\n\n  /** Factory for {@link AdaptiveTrackSelection} instances. */\n  public static class Factory implements TrackSelection.Factory {\n\n    @Nullable private final BandwidthMeter bandwidthMeter;\n    private final int minDurationForQualityIncreaseMs;\n    private final int maxDurationForQualityDecreaseMs;\n    private final int minDurationToRetainAfterDiscardMs;\n    private final float bandwidthFraction;\n    private final float bufferedFractionToLiveEdgeForQualityIncrease;\n    private final long minTimeBetweenBufferReevaluationMs;\n    private final Clock clock;\n\n    /** Creates an adaptive track selection factory with default parameters. */\n    public Factory() {\n      this(\n          DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,\n          DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n          DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n          DEFAULT_BANDWIDTH_FRACTION,\n          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed\n     *     to the player in {@link SimpleExoPlayer.Builder}.\n     */\n    @Deprecated\n    @SuppressWarnings(\"deprecation\")\n    public Factory(BandwidthMeter bandwidthMeter) {\n      this(\n          bandwidthMeter,\n          DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,\n          DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n          DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n          DEFAULT_BANDWIDTH_FRACTION,\n          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * Creates an adaptive track selection factory.\n     *\n     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the\n     *     selected track to switch to one of higher quality.\n     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the\n     *     selected track to switch to one of lower quality.\n     * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher\n     *     quality, the selection may indicate that media already buffered at the lower quality can\n     *     be discarded to speed up the switch. This is the minimum duration of media that must be\n     *     retained at the lower quality.\n     * @param bandwidthFraction The fraction of the available bandwidth that the selection should\n     *     consider available for use. Setting to a value less than 1 is recommended to account for\n     *     inaccuracies in the bandwidth estimator.\n     */\n    public Factory(\n        int minDurationForQualityIncreaseMs,\n        int maxDurationForQualityDecreaseMs,\n        int minDurationToRetainAfterDiscardMs,\n        float bandwidthFraction) {\n      this(\n          minDurationForQualityIncreaseMs,\n          maxDurationForQualityDecreaseMs,\n          minDurationToRetainAfterDiscardMs,\n          bandwidthFraction,\n          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should\n     *     be directly passed to the player in {@link SimpleExoPlayer.Builder}.\n     */\n    @Deprecated\n    @SuppressWarnings(\"deprecation\")\n    public Factory(\n        BandwidthMeter bandwidthMeter,\n        int minDurationForQualityIncreaseMs,\n        int maxDurationForQualityDecreaseMs,\n        int minDurationToRetainAfterDiscardMs,\n        float bandwidthFraction) {\n      this(\n          bandwidthMeter,\n          minDurationForQualityIncreaseMs,\n          maxDurationForQualityDecreaseMs,\n          minDurationToRetainAfterDiscardMs,\n          bandwidthFraction,\n          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n          Clock.DEFAULT);\n    }\n\n    /**\n     * Creates an adaptive track selection factory.\n     *\n     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the\n     *     selected track to switch to one of higher quality.\n     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the\n     *     selected track to switch to one of lower quality.\n     * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher\n     *     quality, the selection may indicate that media already buffered at the lower quality can\n     *     be discarded to speed up the switch. This is the minimum duration of media that must be\n     *     retained at the lower quality.\n     * @param bandwidthFraction The fraction of the available bandwidth that the selection should\n     *     consider available for use. Setting to a value less than 1 is recommended to account for\n     *     inaccuracies in the bandwidth estimator.\n     * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the\n     *     duration from current playback position to the live edge that has to be buffered before\n     *     the selected track can be switched to one of higher quality. This parameter is only\n     *     applied when the playback position is closer to the live edge than {@code\n     *     minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher\n     *     quality from happening.\n     * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its\n     *     buffer and discard some chunks of lower quality to improve the playback quality if\n     *     network conditions have changed. This is the minimum duration between 2 consecutive\n     *     buffer reevaluation calls.\n     * @param clock A {@link Clock}.\n     */\n    @SuppressWarnings(\"deprecation\")\n    public Factory(\n        int minDurationForQualityIncreaseMs,\n        int maxDurationForQualityDecreaseMs,\n        int minDurationToRetainAfterDiscardMs,\n        float bandwidthFraction,\n        float bufferedFractionToLiveEdgeForQualityIncrease,\n        long minTimeBetweenBufferReevaluationMs,\n        Clock clock) {\n      this(\n          /* bandwidthMeter= */ null,\n          minDurationForQualityIncreaseMs,\n          maxDurationForQualityDecreaseMs,\n          minDurationToRetainAfterDiscardMs,\n          bandwidthFraction,\n          bufferedFractionToLiveEdgeForQualityIncrease,\n          minTimeBetweenBufferReevaluationMs,\n          clock);\n    }\n\n    /**\n     * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom\n     *     bandwidth meter should be directly passed to the player in {@link\n     *     SimpleExoPlayer.Builder}.\n     */\n    @Deprecated\n    public Factory(\n        @Nullable BandwidthMeter bandwidthMeter,\n        int minDurationForQualityIncreaseMs,\n        int maxDurationForQualityDecreaseMs,\n        int minDurationToRetainAfterDiscardMs,\n        float bandwidthFraction,\n        float bufferedFractionToLiveEdgeForQualityIncrease,\n        long minTimeBetweenBufferReevaluationMs,\n        Clock clock) {\n      this.bandwidthMeter = bandwidthMeter;\n      this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;\n      this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;\n      this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;\n      this.bandwidthFraction = bandwidthFraction;\n      this.bufferedFractionToLiveEdgeForQualityIncrease =\n          bufferedFractionToLiveEdgeForQualityIncrease;\n      this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;\n      this.clock = clock;\n    }\n\n    @Override\n    public final @NullableType TrackSelection[] createTrackSelections(\n        @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n      if (this.bandwidthMeter != null) {\n        bandwidthMeter = this.bandwidthMeter;\n      }\n      TrackSelection[] selections = new TrackSelection[definitions.length];\n      int totalFixedBandwidth = 0;\n      for (int i = 0; i < definitions.length; i++) {\n        Definition definition = definitions[i];\n        if (definition != null && definition.tracks.length == 1) {\n          // Make fixed selections first to know their total bandwidth.\n          selections[i] =\n              new FixedTrackSelection(\n                  definition.group, definition.tracks[0], definition.reason, definition.data);\n          int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;\n          if (trackBitrate != Format.NO_VALUE) {\n            totalFixedBandwidth += trackBitrate;\n          }\n        }\n      }\n      List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>();\n      for (int i = 0; i < definitions.length; i++) {\n        Definition definition = definitions[i];\n        if (definition != null && definition.tracks.length > 1) {\n          AdaptiveTrackSelection adaptiveSelection =\n              createAdaptiveTrackSelection(\n                  definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth);\n          adaptiveSelections.add(adaptiveSelection);\n          selections[i] = adaptiveSelection;\n        }\n      }\n      if (adaptiveSelections.size() > 1) {\n        long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][];\n        for (int i = 0; i < adaptiveSelections.size(); i++) {\n          AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i);\n          adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()];\n          for (int j = 0; j < adaptiveSelection.length(); j++) {\n            adaptiveTrackBitrates[i][j] =\n                adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate;\n          }\n        }\n        long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates);\n        for (int i = 0; i < adaptiveSelections.size(); i++) {\n          adaptiveSelections\n              .get(i)\n              .experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]);\n        }\n      }\n      return selections;\n    }\n\n    /**\n     * Creates a single adaptive selection for the given group, bandwidth meter and tracks.\n     *\n     * @param group The {@link TrackGroup}.\n     * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.\n     * @param tracks The indices of the selected tracks in the track group.\n     * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits\n     *     per second.\n     * @return An {@link AdaptiveTrackSelection} for the specified tracks.\n     */\n    protected AdaptiveTrackSelection createAdaptiveTrackSelection(\n        TrackGroup group,\n        BandwidthMeter bandwidthMeter,\n        int[] tracks,\n        int totalFixedTrackBandwidth) {\n      return new AdaptiveTrackSelection(\n          group,\n          tracks,\n          new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth),\n          minDurationForQualityIncreaseMs,\n          maxDurationForQualityDecreaseMs,\n          minDurationToRetainAfterDiscardMs,\n          bufferedFractionToLiveEdgeForQualityIncrease,\n          minTimeBetweenBufferReevaluationMs,\n          clock);\n    }\n  }\n\n  public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;\n  public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;\n  public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;\n  public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f;\n  public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;\n  public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;\n\n  private final BandwidthProvider bandwidthProvider;\n  private final long minDurationForQualityIncreaseUs;\n  private final long maxDurationForQualityDecreaseUs;\n  private final long minDurationToRetainAfterDiscardUs;\n  private final float bufferedFractionToLiveEdgeForQualityIncrease;\n  private final long minTimeBetweenBufferReevaluationMs;\n  private final Clock clock;\n\n  private float playbackSpeed;\n  private int selectedIndex;\n  private int reason;\n  private long lastBufferEvaluationMs;\n\n  /**\n   * @param group The {@link TrackGroup}.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     empty. May be in any order.\n   * @param bandwidthMeter Provides an estimate of the currently available bandwidth.\n   */\n  public AdaptiveTrackSelection(TrackGroup group, int[] tracks,\n      BandwidthMeter bandwidthMeter) {\n    this(\n        group,\n        tracks,\n        bandwidthMeter,\n        /* reservedBandwidth= */ 0,\n        DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,\n        DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n        DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n        DEFAULT_BANDWIDTH_FRACTION,\n        DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n        DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n        Clock.DEFAULT);\n  }\n\n  /**\n   * @param group The {@link TrackGroup}.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     empty. May be in any order.\n   * @param bandwidthMeter Provides an estimate of the currently available bandwidth.\n   * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for\n   *     use, in bits per second.\n   * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the\n   *     selected track to switch to one of higher quality.\n   * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the\n   *     selected track to switch to one of lower quality.\n   * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher\n   *     quality, the selection may indicate that media already buffered at the lower quality can be\n   *     discarded to speed up the switch. This is the minimum duration of media that must be\n   *     retained at the lower quality.\n   * @param bandwidthFraction The fraction of the available bandwidth that the selection should\n   *     consider available for use. Setting to a value less than 1 is recommended to account for\n   *     inaccuracies in the bandwidth estimator.\n   * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the\n   *     duration from current playback position to the live edge that has to be buffered before the\n   *     selected track can be switched to one of higher quality. This parameter is only applied\n   *     when the playback position is closer to the live edge than {@code\n   *     minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher\n   *     quality from happening.\n   * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its\n   *     buffer and discard some chunks of lower quality to improve the playback quality if network\n   *     condition has changed. This is the minimum duration between 2 consecutive buffer\n   *     reevaluation calls.\n   */\n  public AdaptiveTrackSelection(\n      TrackGroup group,\n      int[] tracks,\n      BandwidthMeter bandwidthMeter,\n      long reservedBandwidth,\n      long minDurationForQualityIncreaseMs,\n      long maxDurationForQualityDecreaseMs,\n      long minDurationToRetainAfterDiscardMs,\n      float bandwidthFraction,\n      float bufferedFractionToLiveEdgeForQualityIncrease,\n      long minTimeBetweenBufferReevaluationMs,\n      Clock clock) {\n    this(\n        group,\n        tracks,\n        new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth),\n        minDurationForQualityIncreaseMs,\n        maxDurationForQualityDecreaseMs,\n        minDurationToRetainAfterDiscardMs,\n        bufferedFractionToLiveEdgeForQualityIncrease,\n        minTimeBetweenBufferReevaluationMs,\n        clock);\n  }\n\n  private AdaptiveTrackSelection(\n      TrackGroup group,\n      int[] tracks,\n      BandwidthProvider bandwidthProvider,\n      long minDurationForQualityIncreaseMs,\n      long maxDurationForQualityDecreaseMs,\n      long minDurationToRetainAfterDiscardMs,\n      float bufferedFractionToLiveEdgeForQualityIncrease,\n      long minTimeBetweenBufferReevaluationMs,\n      Clock clock) {\n    super(group, tracks);\n    this.bandwidthProvider = bandwidthProvider;\n    this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;\n    this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;\n    this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;\n    this.bufferedFractionToLiveEdgeForQualityIncrease =\n        bufferedFractionToLiveEdgeForQualityIncrease;\n    this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;\n    this.clock = clock;\n    playbackSpeed = 1f;\n    reason = C.SELECTION_REASON_UNKNOWN;\n    lastBufferEvaluationMs = C.TIME_UNSET;\n  }\n\n  /**\n   * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth.\n   *\n   * @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0]\n   *     being the total bandwidth and [1] being the allocated bandwidth.\n   */\n  public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) {\n    ((DefaultBandwidthProvider) bandwidthProvider)\n        .experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints);\n  }\n\n  @Override\n  public void enable() {\n    lastBufferEvaluationMs = C.TIME_UNSET;\n  }\n\n  @Override\n  public void onPlaybackSpeed(float playbackSpeed) {\n    this.playbackSpeed = playbackSpeed;\n  }\n\n  @Override\n  public void updateSelectedTrack(\n      long playbackPositionUs,\n      long bufferedDurationUs,\n      long availableDurationUs,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] mediaChunkIterators) {\n    long nowMs = clock.elapsedRealtime();\n\n    // Make initial selection\n    if (reason == C.SELECTION_REASON_UNKNOWN) {\n      reason = C.SELECTION_REASON_INITIAL;\n      selectedIndex = determineIdealSelectedIndex(nowMs);\n      return;\n    }\n\n    // Stash the current selection, then make a new one.\n    int currentSelectedIndex = selectedIndex;\n    selectedIndex = determineIdealSelectedIndex(nowMs);\n    if (selectedIndex == currentSelectedIndex) {\n      return;\n    }\n\n    if (!isBlacklisted(currentSelectedIndex, nowMs)) {\n      // Revert back to the current selection if conditions are not suitable for switching.\n      Format currentFormat = getFormat(currentSelectedIndex);\n      Format selectedFormat = getFormat(selectedIndex);\n      if (selectedFormat.bitrate > currentFormat.bitrate\n          && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) {\n        // The selected track is a higher quality, but we have insufficient buffer to safely switch\n        // up. Defer switching up for now.\n        selectedIndex = currentSelectedIndex;\n      } else if (selectedFormat.bitrate < currentFormat.bitrate\n          && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {\n        // The selected track is a lower quality, but we have sufficient buffer to defer switching\n        // down for now.\n        selectedIndex = currentSelectedIndex;\n      }\n    }\n    // If we adapted, update the trigger.\n    if (selectedIndex != currentSelectedIndex) {\n      reason = C.SELECTION_REASON_ADAPTIVE;\n    }\n  }\n\n  @Override\n  public int getSelectedIndex() {\n    return selectedIndex;\n  }\n\n  @Override\n  public int getSelectionReason() {\n    return reason;\n  }\n\n  @Override\n  @Nullable\n  public Object getSelectionData() {\n    return null;\n  }\n\n  @Override\n  public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\n    long nowMs = clock.elapsedRealtime();\n    if (!shouldEvaluateQueueSize(nowMs)) {\n      return queue.size();\n    }\n\n    lastBufferEvaluationMs = nowMs;\n    if (queue.isEmpty()) {\n      return 0;\n    }\n\n    int queueSize = queue.size();\n    MediaChunk lastChunk = queue.get(queueSize - 1);\n    long playoutBufferedDurationBeforeLastChunkUs =\n        Util.getPlayoutDurationForMediaDuration(\n            lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);\n    long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs();\n    if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {\n      return queueSize;\n    }\n    int idealSelectedIndex = determineIdealSelectedIndex(nowMs);\n    Format idealFormat = getFormat(idealSelectedIndex);\n    // If the chunks contain video, discard from the first SD chunk beyond\n    // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal\n    // track.\n    for (int i = 0; i < queueSize; i++) {\n      MediaChunk chunk = queue.get(i);\n      Format format = chunk.trackFormat;\n      long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;\n      long playoutDurationBeforeThisChunkUs =\n          Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed);\n      if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs\n          && format.bitrate < idealFormat.bitrate\n          && format.height != Format.NO_VALUE && format.height < 720\n          && format.width != Format.NO_VALUE && format.width < 1280\n          && format.height < idealFormat.height) {\n        return i;\n      }\n    }\n    return queueSize;\n  }\n\n  /**\n   * Called when updating the selected track to determine whether a candidate track can be selected.\n   *\n   * @param format The {@link Format} of the candidate track.\n   * @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate}\n   *     if a more accurate estimate of the current track bitrate is available.\n   * @param playbackSpeed The current playback speed.\n   * @param effectiveBitrate The bitrate available to this selection.\n   * @return Whether this {@link Format} can be selected.\n   */\n  @SuppressWarnings(\"unused\")\n  protected boolean canSelectFormat(\n      Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) {\n    return Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate;\n  }\n\n  /**\n   * Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be\n   * performed.\n   *\n   * @param nowMs The current value of {@link Clock#elapsedRealtime()}.\n   * @return Whether an evaluation should be performed.\n   */\n  protected boolean shouldEvaluateQueueSize(long nowMs) {\n    return lastBufferEvaluationMs == C.TIME_UNSET\n        || nowMs - lastBufferEvaluationMs >= minTimeBetweenBufferReevaluationMs;\n  }\n\n  /**\n   * Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer\n   * to retain after discarding chunks.\n   *\n   * @return The minimum duration of buffer to retain after discarding chunks, in microseconds.\n   */\n  protected long getMinDurationToRetainAfterDiscardUs() {\n    return minDurationToRetainAfterDiscardUs;\n  }\n\n  /**\n   * Computes the ideal selected index ignoring buffer health.\n   *\n   * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link\n   *     Long#MIN_VALUE} to ignore blacklisting.\n   */\n  private int determineIdealSelectedIndex(long nowMs) {\n    long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth();\n    int lowestBitrateNonBlacklistedIndex = 0;\n    for (int i = 0; i < length; i++) {\n      if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {\n        Format format = getFormat(i);\n        if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) {\n          return i;\n        } else {\n          lowestBitrateNonBlacklistedIndex = i;\n        }\n      }\n    }\n    return lowestBitrateNonBlacklistedIndex;\n  }\n\n  private long minDurationForQualityIncreaseUs(long availableDurationUs) {\n    boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET\n        && availableDurationUs <= minDurationForQualityIncreaseUs;\n    return isAvailableDurationTooShort\n        ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease)\n        : minDurationForQualityIncreaseUs;\n  }\n\n  /** Provides the allocated bandwidth. */\n  private interface BandwidthProvider {\n\n    /** Returns the allocated bitrate. */\n    long getAllocatedBandwidth();\n  }\n\n  private static final class DefaultBandwidthProvider implements BandwidthProvider {\n\n    private final BandwidthMeter bandwidthMeter;\n    private final float bandwidthFraction;\n    private final long reservedBandwidth;\n\n    @Nullable private long[][] allocationCheckpoints;\n\n    /* package */\n    // the constructor does not initialize fields: allocationCheckpoints\n    @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n    DefaultBandwidthProvider(\n        BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) {\n      this.bandwidthMeter = bandwidthMeter;\n      this.bandwidthFraction = bandwidthFraction;\n      this.reservedBandwidth = reservedBandwidth;\n    }\n\n    // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0]\n    @SuppressWarnings(\"nullness:unboxing.of.nullable\")\n    @Override\n    public long getAllocatedBandwidth() {\n      long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);\n      long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth);\n      if (allocationCheckpoints == null) {\n        return allocatableBandwidth;\n      }\n      int nextIndex = 1;\n      while (nextIndex < allocationCheckpoints.length - 1\n          && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {\n        nextIndex++;\n      }\n      long[] previous = allocationCheckpoints[nextIndex - 1];\n      long[] next = allocationCheckpoints[nextIndex];\n      float fractionBetweenCheckpoints =\n          (float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]);\n      return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1]));\n    }\n\n    /* package */ void experimental_setBandwidthAllocationCheckpoints(\n        long[][] allocationCheckpoints) {\n      Assertions.checkArgument(allocationCheckpoints.length >= 2);\n      this.allocationCheckpoints = allocationCheckpoints;\n    }\n  }\n\n  /**\n   * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track\n   * selections.\n   *\n   * @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate.\n   * @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total\n   *     bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint.\n   */\n  private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) {\n    // Algorithm:\n    //  1. Use log bitrates to treat all resolution update steps equally.\n    //  2. Distribute switch points for each selection equally in the same [0.0-1.0] range.\n    //  3. Switch up one format at a time in the order of the switch points.\n    double[][] logBitrates = getLogArrayValues(trackBitrates);\n    double[][] switchPoints = getSwitchPoints(logBitrates);\n\n    // There will be (count(switch point) + 3) checkpoints:\n    // [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points,\n    // [end] = extra point to set slope for additional bitrate.\n    int checkpointCount = countArrayElements(switchPoints) + 3;\n    long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2];\n    int[] currentSelection = new int[logBitrates.length];\n    setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection);\n    for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) {\n      int nextUpdateIndex = 0;\n      double nextUpdateSwitchPoint = Double.MAX_VALUE;\n      for (int i = 0; i < logBitrates.length; i++) {\n        if (currentSelection[i] + 1 == logBitrates[i].length) {\n          continue;\n        }\n        double switchPoint = switchPoints[i][currentSelection[i]];\n        if (switchPoint < nextUpdateSwitchPoint) {\n          nextUpdateSwitchPoint = switchPoint;\n          nextUpdateIndex = i;\n        }\n      }\n      currentSelection[nextUpdateIndex]++;\n      setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection);\n    }\n    for (long[][] points : checkpoints) {\n      points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0];\n      points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1];\n    }\n    return checkpoints;\n  }\n\n  /** Converts all input values to Math.log(value). */\n  private static double[][] getLogArrayValues(long[][] values) {\n    double[][] logValues = new double[values.length][];\n    for (int i = 0; i < values.length; i++) {\n      logValues[i] = new double[values[i].length];\n      for (int j = 0; j < values[i].length; j++) {\n        logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]);\n      }\n    }\n    return logValues;\n  }\n\n  /**\n   * Returns idealized switch points for each switch between consecutive track selection bitrates.\n   *\n   * @param logBitrates Log bitrates with [selectionCount][formatCount].\n   * @return Linearly distributed switch points in the range of [0.0-1.0].\n   */\n  private static double[][] getSwitchPoints(double[][] logBitrates) {\n    double[][] switchPoints = new double[logBitrates.length][];\n    for (int i = 0; i < logBitrates.length; i++) {\n      switchPoints[i] = new double[logBitrates[i].length - 1];\n      if (switchPoints[i].length == 0) {\n        continue;\n      }\n      double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];\n      for (int j = 0; j < logBitrates[i].length - 1; j++) {\n        double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);\n        switchPoints[i][j] =\n            totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;\n      }\n    }\n    return switchPoints;\n  }\n\n  /** Returns total number of elements in a 2D array. */\n  private static int countArrayElements(double[][] array) {\n    int count = 0;\n    for (double[] subArray : array) {\n      count += subArray.length;\n    }\n    return count;\n  }\n\n  /**\n   * Sets checkpoint bitrates.\n   *\n   * @param checkpoints Output checkpoints with [selectionIndex][checkpointIndex][2] where [0]=Total\n   *     bitrate and [1]=Allocated bitrate.\n   * @param checkpointIndex The checkpoint index.\n   * @param trackBitrates The track bitrates with [selectionIndex][trackIndex].\n   * @param selectedTracks The indices of selected tracks for each selection for this checkpoint.\n   */\n  private static void setCheckpointValues(\n      long[][][] checkpoints, int checkpointIndex, long[][] trackBitrates, int[] selectedTracks) {\n    long totalBitrate = 0;\n    for (int i = 0; i < checkpoints.length; i++) {\n      checkpoints[i][checkpointIndex][1] = trackBitrates[i][selectedTracks[i]];\n      totalBitrate += checkpoints[i][checkpointIndex][1];\n    }\n    for (long[][] points : checkpoints) {\n      points[checkpointIndex][0] = totalBitrate;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\n\n/**\n * An abstract base class suitable for most {@link TrackSelection} implementations.\n */\npublic abstract class BaseTrackSelection implements TrackSelection {\n\n  /**\n   * The selected {@link TrackGroup}.\n   */\n  protected final TrackGroup group;\n  /**\n   * The number of selected tracks within the {@link TrackGroup}. Always greater than zero.\n   */\n  protected final int length;\n  /**\n   * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.\n   */\n  protected final int[] tracks;\n\n  /**\n   * The {@link Format}s of the selected tracks, in order of decreasing bandwidth.\n   */\n  private final Format[] formats;\n  /**\n   * Selected track blacklist timestamps, in order of decreasing bandwidth.\n   */\n  private final long[] blacklistUntilTimes;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     null or empty. May be in any order.\n   */\n  public BaseTrackSelection(TrackGroup group, int... tracks) {\n    Assertions.checkState(tracks.length > 0);\n    this.group = Assertions.checkNotNull(group);\n    this.length = tracks.length;\n    // Set the formats, sorted in order of decreasing bandwidth.\n    formats = new Format[length];\n    for (int i = 0; i < tracks.length; i++) {\n      formats[i] = group.getFormat(tracks[i]);\n    }\n    Arrays.sort(formats, new DecreasingBandwidthComparator());\n    // Set the format indices in the same order.\n    this.tracks = new int[length];\n    for (int i = 0; i < length; i++) {\n      this.tracks[i] = group.indexOf(formats[i]);\n    }\n    blacklistUntilTimes = new long[length];\n  }\n\n  @Override\n  public void enable() {\n    // Do nothing.\n  }\n\n  @Override\n  public void disable() {\n    // Do nothing.\n  }\n\n  @Override\n  public final TrackGroup getTrackGroup() {\n    return group;\n  }\n\n  @Override\n  public final int length() {\n    return tracks.length;\n  }\n\n  @Override\n  public final Format getFormat(int index) {\n    return formats[index];\n  }\n\n  @Override\n  public final int getIndexInTrackGroup(int index) {\n    return tracks[index];\n  }\n\n  @Override\n  @SuppressWarnings(\"ReferenceEquality\")\n  public final int indexOf(Format format) {\n    for (int i = 0; i < length; i++) {\n      if (formats[i] == format) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public final int indexOf(int indexInTrackGroup) {\n    for (int i = 0; i < length; i++) {\n      if (tracks[i] == indexInTrackGroup) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public final Format getSelectedFormat() {\n    return formats[getSelectedIndex()];\n  }\n\n  @Override\n  public final int getSelectedIndexInTrackGroup() {\n    return tracks[getSelectedIndex()];\n  }\n\n  @Override\n  public void onPlaybackSpeed(float playbackSpeed) {\n    // Do nothing.\n  }\n\n  @Override\n  public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\n    return queue.size();\n  }\n\n  @Override\n  public final boolean blacklist(int index, long blacklistDurationMs) {\n    long nowMs = SystemClock.elapsedRealtime();\n    boolean canBlacklist = isBlacklisted(index, nowMs);\n    for (int i = 0; i < length && !canBlacklist; i++) {\n      canBlacklist = i != index && !isBlacklisted(i, nowMs);\n    }\n    if (!canBlacklist) {\n      return false;\n    }\n    blacklistUntilTimes[index] =\n        Math.max(\n            blacklistUntilTimes[index],\n            Util.addWithOverflowDefault(nowMs, blacklistDurationMs, Long.MAX_VALUE));\n    return true;\n  }\n\n  /**\n   * Returns whether the track at the specified index in the selection is blacklisted.\n   *\n   * @param index The index of the track in the selection.\n   * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}.\n   */\n  protected final boolean isBlacklisted(int index, long nowMs) {\n    return blacklistUntilTimes[index] > nowMs;\n  }\n\n  // Object overrides.\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);\n    }\n    return hashCode;\n  }\n\n  // Track groups are compared by identity not value, as distinct groups may have the same value.\n  @Override\n  @SuppressWarnings({\"ReferenceEquality\", \"EqualsGetClass\"})\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    BaseTrackSelection other = (BaseTrackSelection) obj;\n    return group == other.group && Arrays.equals(tracks, other.tracks);\n  }\n\n  /**\n   * Sorts {@link Format} objects in order of decreasing bandwidth.\n   */\n  private static final class DecreasingBandwidthComparator implements Comparator<Format> {\n\n    @Override\n    public int compare(Format a, Format b) {\n      return b.bitrate - a.bitrate;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.DefaultLoadControl;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.LoadControl;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DefaultAllocator;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size\n * based track adaptation.\n */\npublic final class BufferSizeAdaptationBuilder {\n\n  /** Dynamic filter for formats, which is applied when selecting a new track. */\n  public interface DynamicFormatFilter {\n\n    /** Filter which allows all formats. */\n    DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true;\n\n    /**\n     * Called when updating the selected track to determine whether a candidate track is allowed. If\n     * no format is allowed or eligible, the lowest quality format will be used.\n     *\n     * @param format The {@link Format} of the candidate track.\n     * @param trackBitrate The estimated bitrate of the track. May differ from {@link\n     *     Format#bitrate} if a more accurate estimate of the current track bitrate is available.\n     * @param isInitialSelection Whether this is for the initial track selection.\n     */\n    boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection);\n  }\n\n  /**\n   * The default minimum duration of media that the player will attempt to ensure is buffered at all\n   * times, in milliseconds.\n   */\n  public static final int DEFAULT_MIN_BUFFER_MS = 15000;\n\n  /**\n   * The default maximum duration of media that the player will attempt to buffer, in milliseconds.\n   */\n  public static final int DEFAULT_MAX_BUFFER_MS = 50000;\n\n  /**\n   * The default duration of media that must be buffered for playback to start or resume following a\n   * user action such as a seek, in milliseconds.\n   */\n  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS =\n      DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;\n\n  /**\n   * The default duration of media that must be buffered for playback to resume after a rebuffer, in\n   * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.\n   */\n  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS =\n      DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;\n\n  /**\n   * The default offset the current duration of buffered media must deviate from the ideal duration\n   * of buffered media for the currently selected format, before the selected format is changed.\n   */\n  public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000;\n\n  /**\n   * During start-up phase, the default fraction of the available bandwidth that the selection\n   * should consider available for use. Setting to a value less than 1 is recommended to account for\n   * inaccuracies in the bandwidth estimator.\n   */\n  public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION =\n      AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION;\n\n  /**\n   * During start-up phase, the default minimum duration of buffered media required for the selected\n   * track to switch to one of higher quality based on measured bandwidth.\n   */\n  public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS =\n      AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS;\n\n  @Nullable private DefaultAllocator allocator;\n  private Clock clock;\n  private int minBufferMs;\n  private int maxBufferMs;\n  private int bufferForPlaybackMs;\n  private int bufferForPlaybackAfterRebufferMs;\n  private int hysteresisBufferMs;\n  private float startUpBandwidthFraction;\n  private int startUpMinBufferForQualityIncreaseMs;\n  private DynamicFormatFilter dynamicFormatFilter;\n  private boolean buildCalled;\n\n  /** Creates builder with default values. */\n  public BufferSizeAdaptationBuilder() {\n    clock = Clock.DEFAULT;\n    minBufferMs = DEFAULT_MIN_BUFFER_MS;\n    maxBufferMs = DEFAULT_MAX_BUFFER_MS;\n    bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS;\n    bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;\n    hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS;\n    startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION;\n    startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS;\n    dynamicFormatFilter = DynamicFormatFilter.NO_FILTER;\n  }\n\n  /**\n   * Set the clock to use. Should only be set for testing purposes.\n   *\n   * @param clock The {@link Clock}.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setClock(Clock clock) {\n    Assertions.checkState(!buildCalled);\n    this.clock = clock;\n    return this;\n  }\n\n  /**\n   * Sets the {@link DefaultAllocator} used by the loader.\n   *\n   * @param allocator The {@link DefaultAllocator}.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) {\n    Assertions.checkState(!buildCalled);\n    this.allocator = allocator;\n    return this;\n  }\n\n  /**\n   * Sets the buffer duration parameters.\n   *\n   * @param minBufferMs The minimum duration of media that the player will attempt to ensure is\n   *     buffered at all times, in milliseconds.\n   * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in\n   *     milliseconds.\n   * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or\n   *     resume following a user action such as a seek, in milliseconds.\n   * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for\n   *     playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by\n   *     buffer depletion rather than a user action.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setBufferDurationsMs(\n      int minBufferMs,\n      int maxBufferMs,\n      int bufferForPlaybackMs,\n      int bufferForPlaybackAfterRebufferMs) {\n    Assertions.checkState(!buildCalled);\n    this.minBufferMs = minBufferMs;\n    this.maxBufferMs = maxBufferMs;\n    this.bufferForPlaybackMs = bufferForPlaybackMs;\n    this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;\n    return this;\n  }\n\n  /**\n   * Sets the hysteresis buffer used to prevent repeated format switching.\n   *\n   * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from\n   *     the ideal duration of buffered media for the currently selected format, before the selected\n   *     format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) {\n    Assertions.checkState(!buildCalled);\n    this.hysteresisBufferMs = hysteresisBufferMs;\n    return this;\n  }\n\n  /**\n   * Sets track selection parameters used during the start-up phase before the selection can be made\n   * purely on based on buffer size. During the start-up phase the selection is based on the current\n   * bandwidth estimate.\n   *\n   * @param bandwidthFraction The fraction of the available bandwidth that the selection should\n   *     consider available for use. Setting to a value less than 1 is recommended to account for\n   *     inaccuracies in the bandwidth estimator.\n   * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the\n   *     selected track to switch to one of higher quality.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters(\n      float bandwidthFraction, int minBufferForQualityIncreaseMs) {\n    Assertions.checkState(!buildCalled);\n    this.startUpBandwidthFraction = bandwidthFraction;\n    this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs;\n    return this;\n  }\n\n  /**\n   * Sets the {@link DynamicFormatFilter} to use when updating the selected track.\n   *\n   * @param dynamicFormatFilter The {@link DynamicFormatFilter}.\n   * @return This builder, for convenience.\n   * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.\n   */\n  public BufferSizeAdaptationBuilder setDynamicFormatFilter(\n      DynamicFormatFilter dynamicFormatFilter) {\n    Assertions.checkState(!buildCalled);\n    this.dynamicFormatFilter = dynamicFormatFilter;\n    return this;\n  }\n\n  /**\n   * Builds player components for buffer size based track adaptation.\n   *\n   * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be\n   *     used to construct the player.\n   */\n  public Pair<TrackSelection.Factory, LoadControl> buildPlayerComponents() {\n    Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs);\n    Assertions.checkState(!buildCalled);\n    buildCalled = true;\n\n    DefaultLoadControl.Builder loadControlBuilder =\n        new DefaultLoadControl.Builder()\n            .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE)\n            .setBufferDurationsMs(\n                /* minBufferMs= */ maxBufferMs,\n                maxBufferMs,\n                bufferForPlaybackMs,\n                bufferForPlaybackAfterRebufferMs);\n    if (allocator != null) {\n      loadControlBuilder.setAllocator(allocator);\n    }\n\n    TrackSelection.Factory trackSelectionFactory =\n        new TrackSelection.Factory() {\n          @Override\n          public @NullableType TrackSelection[] createTrackSelections(\n              @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n            return TrackSelectionUtil.createTrackSelectionsForDefinitions(\n                definitions,\n                definition ->\n                    new BufferSizeAdaptiveTrackSelection(\n                        definition.group,\n                        definition.tracks,\n                        bandwidthMeter,\n                        minBufferMs,\n                        maxBufferMs,\n                        hysteresisBufferMs,\n                        startUpBandwidthFraction,\n                        startUpMinBufferForQualityIncreaseMs,\n                        dynamicFormatFilter,\n                        clock));\n          }\n        };\n\n    return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl());\n  }\n\n  private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection {\n\n    private static final int BITRATE_BLACKLISTED = Format.NO_VALUE;\n\n    private final BandwidthMeter bandwidthMeter;\n    private final Clock clock;\n    private final DynamicFormatFilter dynamicFormatFilter;\n    private final int[] formatBitrates;\n    private final long minBufferUs;\n    private final long maxBufferUs;\n    private final long hysteresisBufferUs;\n    private final float startUpBandwidthFraction;\n    private final long startUpMinBufferForQualityIncreaseUs;\n    private final int minBitrate;\n    private final int maxBitrate;\n    private final double bitrateToBufferFunctionSlope;\n    private final double bitrateToBufferFunctionIntercept;\n\n    private boolean isInSteadyState;\n    private int selectedIndex;\n    private int selectionReason;\n    private float playbackSpeed;\n\n    private BufferSizeAdaptiveTrackSelection(\n        TrackGroup trackGroup,\n        int[] tracks,\n        BandwidthMeter bandwidthMeter,\n        int minBufferMs,\n        int maxBufferMs,\n        int hysteresisBufferMs,\n        float startUpBandwidthFraction,\n        int startUpMinBufferForQualityIncreaseMs,\n        DynamicFormatFilter dynamicFormatFilter,\n        Clock clock) {\n      super(trackGroup, tracks);\n      this.bandwidthMeter = bandwidthMeter;\n      this.minBufferUs = C.msToUs(minBufferMs);\n      this.maxBufferUs = C.msToUs(maxBufferMs);\n      this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs);\n      this.startUpBandwidthFraction = startUpBandwidthFraction;\n      this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs);\n      this.dynamicFormatFilter = dynamicFormatFilter;\n      this.clock = clock;\n\n      formatBitrates = new int[length];\n      maxBitrate = getFormat(/* index= */ 0).bitrate;\n      minBitrate = getFormat(/* index= */ length - 1).bitrate;\n      selectionReason = C.SELECTION_REASON_UNKNOWN;\n      playbackSpeed = 1.0f;\n\n      // We use a log-linear function to map from bitrate to buffer size:\n      // buffer = slope * ln(bitrate) + intercept,\n      // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer.\n      bitrateToBufferFunctionSlope =\n          (maxBufferUs - hysteresisBufferUs - minBufferUs)\n              / Math.log((double) maxBitrate / minBitrate);\n      bitrateToBufferFunctionIntercept =\n          minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate);\n    }\n\n    @Override\n    public void onPlaybackSpeed(float playbackSpeed) {\n      this.playbackSpeed = playbackSpeed;\n    }\n\n    @Override\n    public void onDiscontinuity() {\n      isInSteadyState = false;\n    }\n\n    @Override\n    public int getSelectedIndex() {\n      return selectedIndex;\n    }\n\n    @Override\n    public int getSelectionReason() {\n      return selectionReason;\n    }\n\n    @Override\n    @Nullable\n    public Object getSelectionData() {\n      return null;\n    }\n\n    @Override\n    public void updateSelectedTrack(\n        long playbackPositionUs,\n        long bufferedDurationUs,\n        long availableDurationUs,\n        List<? extends MediaChunk> queue,\n        MediaChunkIterator[] mediaChunkIterators) {\n      updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime());\n\n      // Make initial selection\n      if (selectionReason == C.SELECTION_REASON_UNKNOWN) {\n        selectionReason = C.SELECTION_REASON_INITIAL;\n        selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true);\n        return;\n      }\n\n      long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs);\n      int oldSelectedIndex = selectedIndex;\n      if (isInSteadyState) {\n        selectIndexSteadyState(bufferUs);\n      } else {\n        selectIndexStartUpPhase(bufferUs);\n      }\n      if (selectedIndex != oldSelectedIndex) {\n        selectionReason = C.SELECTION_REASON_ADAPTIVE;\n      }\n    }\n\n    // Steady state.\n\n    private void selectIndexSteadyState(long bufferUs) {\n      if (isOutsideHysteresis(bufferUs)) {\n        selectedIndex = selectIdealIndexUsingBufferSize(bufferUs);\n      }\n    }\n\n    private boolean isOutsideHysteresis(long bufferUs) {\n      if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) {\n        return true;\n      }\n      long targetBufferForCurrentBitrateUs =\n          getTargetBufferForBitrateUs(formatBitrates[selectedIndex]);\n      long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs;\n      return Math.abs(bufferDiffUs) > hysteresisBufferUs;\n    }\n\n    private int selectIdealIndexUsingBufferSize(long bufferUs) {\n      int lowestBitrateNonBlacklistedIndex = 0;\n      for (int i = 0; i < formatBitrates.length; i++) {\n        if (formatBitrates[i] != BITRATE_BLACKLISTED) {\n          if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs\n              && dynamicFormatFilter.isFormatAllowed(\n                  getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) {\n            return i;\n          }\n          lowestBitrateNonBlacklistedIndex = i;\n        }\n      }\n      return lowestBitrateNonBlacklistedIndex;\n    }\n\n    // Startup.\n\n    private void selectIndexStartUpPhase(long bufferUs) {\n      int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false);\n      int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs);\n      if (steadyStateSelectedIndex <= selectedIndex) {\n        // Switch to steady state if we have enough buffer to maintain current selection.\n        selectedIndex = steadyStateSelectedIndex;\n        isInSteadyState = true;\n      } else {\n        if (bufferUs < startUpMinBufferForQualityIncreaseUs\n            && startUpSelectedIndex < selectedIndex\n            && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) {\n          // Switching up from a non-blacklisted track is only allowed if we have enough buffer.\n          return;\n        }\n        selectedIndex = startUpSelectedIndex;\n      }\n    }\n\n    private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) {\n      long effectiveBitrate =\n          (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction);\n      int lowestBitrateNonBlacklistedIndex = 0;\n      for (int i = 0; i < formatBitrates.length; i++) {\n        if (formatBitrates[i] != BITRATE_BLACKLISTED) {\n          if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate\n              && dynamicFormatFilter.isFormatAllowed(\n                  getFormat(i), formatBitrates[i], isInitialSelection)) {\n            return i;\n          }\n          lowestBitrateNonBlacklistedIndex = i;\n        }\n      }\n      return lowestBitrateNonBlacklistedIndex;\n    }\n\n    // Utility methods.\n\n    private void updateFormatBitrates(long nowMs) {\n      for (int i = 0; i < length; i++) {\n        if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {\n          formatBitrates[i] = getFormat(i).bitrate;\n        } else {\n          formatBitrates[i] = BITRATE_BLACKLISTED;\n        }\n      }\n    }\n\n    private long getTargetBufferForBitrateUs(int bitrate) {\n      if (bitrate <= minBitrate) {\n        return minBufferUs;\n      }\n      if (bitrate >= maxBitrate) {\n        return maxBufferUs - hysteresisBufferUs;\n      }\n      return (int)\n          (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept);\n    }\n\n    private static long getCurrentPeriodBufferedDurationUs(\n        long playbackPositionUs, long bufferedDurationUs) {\n      return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\nimport android.util.Pair;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;\nimport com.google.android.exoplayer2.RendererCapabilities.Capabilities;\nimport com.google.android.exoplayer2.RendererCapabilities.FormatSupport;\nimport com.google.android.exoplayer2.RendererConfiguration;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.checkerframework.checker.initialization.qual.UnderInitialization;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A default {@link TrackSelector} suitable for most use cases. Track selections are made according\n * to configurable {@link Parameters}, which can be set by calling {@link\n * #setParameters(Parameters)}.\n *\n * <h3>Modifying parameters</h3>\n *\n * To modify only some aspects of the parameters currently used by a selector, it's possible to\n * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired\n * modifications can be made on the builder, and the resulting {@link Parameters} can then be built\n * and set on the selector. For example the following code modifies the parameters to restrict video\n * track selections to SD, and to select a German audio track if there is one:\n *\n * <pre>{@code\n * // Build on the current parameters.\n * Parameters currentParameters = trackSelector.getParameters();\n * // Build the resulting parameters.\n * Parameters newParameters = currentParameters\n *     .buildUpon()\n *     .setMaxVideoSizeSd()\n *     .setPreferredAudioLanguage(\"deu\")\n *     .build();\n * // Set the new parameters.\n * trackSelector.setParameters(newParameters);\n * }</pre>\n *\n * Convenience methods and chaining allow this to be written more concisely as:\n *\n * <pre>{@code\n * trackSelector.setParameters(\n *     trackSelector\n *         .buildUponParameters()\n *         .setMaxVideoSizeSd()\n *         .setPreferredAudioLanguage(\"deu\"));\n * }</pre>\n *\n * Selection {@link Parameters} support many different options, some of which are described below.\n *\n * <h3>Selecting specific tracks</h3>\n *\n * Track selection overrides can be used to select specific tracks. To specify an override for a\n * renderer, it's first necessary to obtain the tracks that have been mapped to it:\n *\n * <pre>{@code\n * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();\n * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null\n *     : mappedTrackInfo.getTrackGroups(rendererIndex);\n * }</pre>\n *\n * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so\n * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the\n * player can be used to determine when the current tracks (and therefore the mapping) changes. If\n * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query\n * the properties of the available tracks to determine the {@code groupIndex} and the {@code\n * trackIndices} within the group it that should be selected. The override can then be specified\n * using {@link ParametersBuilder#setSelectionOverride}:\n *\n * <pre>{@code\n * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);\n * trackSelector.setParameters(\n *     trackSelector\n *         .buildUponParameters()\n *         .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));\n * }</pre>\n *\n * <h3>Constraint based track selection</h3>\n *\n * Whilst track selection overrides make it possible to select specific tracks, the recommended way\n * of controlling which tracks are selected is by specifying constraints. For example consider the\n * case of wanting to restrict video track selections to SD, and preferring German audio tracks.\n * Track selection overrides could be used to select specific tracks meeting these criteria, however\n * a simpler and more flexible approach is to specify these constraints directly:\n *\n * <pre>{@code\n * trackSelector.setParameters(\n *     trackSelector\n *         .buildUponParameters()\n *         .setMaxVideoSizeSd()\n *         .setPreferredAudioLanguage(\"deu\"));\n * }</pre>\n *\n * There are several benefits to using constraint based track selection instead of specific track\n * overrides:\n *\n * <ul>\n *   <li>You can specify constraints before knowing what tracks the media provides. This can\n *       simplify track selection code (e.g. you don't have to listen for changes in the available\n *       tracks before configuring the selector).\n *   <li>Constraints can be applied consistently across all periods in a complex piece of media,\n *       even if those periods contain different tracks. In contrast, a specific track override is\n *       only applied to periods whose tracks match those for which the override was set.\n * </ul>\n *\n * <h3>Disabling renderers</h3>\n *\n * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a\n * renderer differs from setting a {@code null} override because the renderer is disabled\n * unconditionally, whereas a {@code null} override is applied only when the track groups available\n * to the renderer match the {@link TrackGroupArray} for which it was specified.\n *\n * <h3>Tunneling</h3>\n *\n * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks\n * support it. Tunneled playback is enabled by passing an audio session ID to {@link\n * ParametersBuilder#setTunnelingAudioSessionId(int)}.\n */\npublic class DefaultTrackSelector extends MappingTrackSelector {\n\n  /**\n   * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of\n   * the parameters that can be configured using this builder.\n   */\n  public static final class ParametersBuilder extends TrackSelectionParameters.Builder {\n\n    // Video\n    private int maxVideoWidth;\n    private int maxVideoHeight;\n    private int maxVideoFrameRate;\n    private int maxVideoBitrate;\n    private boolean exceedVideoConstraintsIfNecessary;\n    private boolean allowVideoMixedMimeTypeAdaptiveness;\n    private boolean allowVideoNonSeamlessAdaptiveness;\n    private int viewportWidth;\n    private int viewportHeight;\n    private boolean viewportOrientationMayChange;\n    // Audio\n    private int maxAudioChannelCount;\n    private int maxAudioBitrate;\n    private boolean exceedAudioConstraintsIfNecessary;\n    private boolean allowAudioMixedMimeTypeAdaptiveness;\n    private boolean allowAudioMixedSampleRateAdaptiveness;\n    private boolean allowAudioMixedChannelCountAdaptiveness;\n    // General\n    private boolean forceLowestBitrate;\n    private boolean forceHighestSupportedBitrate;\n    private boolean exceedRendererCapabilitiesIfNecessary;\n    private int tunnelingAudioSessionId;\n\n    private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>\n        selectionOverrides;\n    private final SparseBooleanArray rendererDisabledFlags;\n\n    /**\n     * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link\n     *     #ParametersBuilder(Context)} instead.\n     */\n    @Deprecated\n    @SuppressWarnings({\"deprecation\"})\n    public ParametersBuilder() {\n      super();\n      setInitialValuesWithoutContext();\n      selectionOverrides = new SparseArray<>();\n      rendererDisabledFlags = new SparseBooleanArray();\n    }\n\n    /**\n     * Creates a builder with default initial values.\n     *\n     * @param context Any context.\n     */\n\n    public ParametersBuilder(Context context) {\n      super(context);\n      setInitialValuesWithoutContext();\n      selectionOverrides = new SparseArray<>();\n      rendererDisabledFlags = new SparseBooleanArray();\n      setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true);\n    }\n\n    /**\n     * @param initialValues The {@link Parameters} from which the initial values of the builder are\n     *     obtained.\n     */\n    private ParametersBuilder(Parameters initialValues) {\n      super(initialValues);\n      // Video\n      maxVideoWidth = initialValues.maxVideoWidth;\n      maxVideoHeight = initialValues.maxVideoHeight;\n      maxVideoFrameRate = initialValues.maxVideoFrameRate;\n      maxVideoBitrate = initialValues.maxVideoBitrate;\n      exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary;\n      allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness;\n      allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness;\n      viewportWidth = initialValues.viewportWidth;\n      viewportHeight = initialValues.viewportHeight;\n      viewportOrientationMayChange = initialValues.viewportOrientationMayChange;\n      // Audio\n      maxAudioChannelCount = initialValues.maxAudioChannelCount;\n      maxAudioBitrate = initialValues.maxAudioBitrate;\n      exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary;\n      allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness;\n      allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness;\n      allowAudioMixedChannelCountAdaptiveness =\n          initialValues.allowAudioMixedChannelCountAdaptiveness;\n      // General\n      forceLowestBitrate = initialValues.forceLowestBitrate;\n      forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate;\n      exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary;\n      tunnelingAudioSessionId = initialValues.tunnelingAudioSessionId;\n      // Overrides\n      selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides);\n      rendererDisabledFlags = initialValues.rendererDisabledFlags.clone();\n    }\n\n    // Video\n\n    /**\n     * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}.\n     *\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxVideoSizeSd() {\n      return setMaxVideoSize(1279, 719);\n    }\n\n    /**\n     * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.\n     *\n     * @return This builder.\n     */\n    public ParametersBuilder clearVideoSizeConstraints() {\n      return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);\n    }\n\n    /**\n     * Sets the maximum allowed video width and height.\n     *\n     * @param maxVideoWidth Maximum allowed video width in pixels.\n     * @param maxVideoHeight Maximum allowed video height in pixels.\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {\n      this.maxVideoWidth = maxVideoWidth;\n      this.maxVideoHeight = maxVideoHeight;\n      return this;\n    }\n\n    /**\n     * Sets the maximum allowed video frame rate.\n     *\n     * @param maxVideoFrameRate Maximum allowed video frame rate in hertz.\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) {\n      this.maxVideoFrameRate = maxVideoFrameRate;\n      return this;\n    }\n\n    /**\n     * Sets the maximum allowed video bitrate.\n     *\n     * @param maxVideoBitrate Maximum allowed video bitrate in bits per second.\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) {\n      this.maxVideoBitrate = maxVideoBitrate;\n      return this;\n    }\n\n    /**\n     * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link\n     * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise.\n     *\n     * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no\n     *     selection can be made otherwise.\n     * @return This builder.\n     */\n    public ParametersBuilder setExceedVideoConstraintsIfNecessary(\n        boolean exceedVideoConstraintsIfNecessary) {\n      this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;\n      return this;\n    }\n\n    /**\n     * Sets whether to allow adaptive video selections containing mixed MIME types.\n     *\n     * <p>Adaptations between different MIME types may not be completely seamless, in which case\n     * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for\n     * mixed MIME type selections to be made.\n     *\n     * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections\n     *     containing mixed MIME types.\n     * @return This builder.\n     */\n    public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness(\n        boolean allowVideoMixedMimeTypeAdaptiveness) {\n      this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;\n      return this;\n    }\n\n    /**\n     * Sets whether to allow adaptive video selections where adaptation may not be completely\n     * seamless.\n     *\n     * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where\n     *     adaptation may not be completely seamless.\n     * @return This builder.\n     */\n    public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness(\n        boolean allowVideoNonSeamlessAdaptiveness) {\n      this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;\n      return this;\n    }\n\n    /**\n     * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size\n     * obtained from {@link Util#getPhysicalDisplaySize(Context)}.\n     *\n     * @param context Any context.\n     * @param viewportOrientationMayChange Whether the viewport orientation may change during\n     *     playback.\n     * @return This builder.\n     */\n    public ParametersBuilder setViewportSizeToPhysicalDisplaySize(\n        Context context, boolean viewportOrientationMayChange) {\n      // Assume the viewport is fullscreen.\n      Point viewportSize = Util.getPhysicalDisplaySize(context);\n      return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);\n    }\n\n    /**\n     * Equivalent to {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE,\n     * true)}.\n     *\n     * @return This builder.\n     */\n    public ParametersBuilder clearViewportSizeConstraints() {\n      return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);\n    }\n\n    /**\n     * Sets the viewport size to constrain adaptive video selections so that only tracks suitable\n     * for the viewport are selected.\n     *\n     * @param viewportWidth Viewport width in pixels.\n     * @param viewportHeight Viewport height in pixels.\n     * @param viewportOrientationMayChange Whether the viewport orientation may change during\n     *     playback.\n     * @return This builder.\n     */\n    public ParametersBuilder setViewportSize(\n        int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {\n      this.viewportWidth = viewportWidth;\n      this.viewportHeight = viewportHeight;\n      this.viewportOrientationMayChange = viewportOrientationMayChange;\n      return this;\n    }\n\n    // Audio\n\n    @Override\n    public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {\n      super.setPreferredAudioLanguage(preferredAudioLanguage);\n      return this;\n    }\n\n    /**\n     * Sets the maximum allowed audio channel count.\n     *\n     * @param maxAudioChannelCount Maximum allowed audio channel count.\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) {\n      this.maxAudioChannelCount = maxAudioChannelCount;\n      return this;\n    }\n\n    /**\n     * Sets the maximum allowed audio bitrate.\n     *\n     * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second.\n     * @return This builder.\n     */\n    public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) {\n      this.maxAudioBitrate = maxAudioBitrate;\n      return this;\n    }\n\n    /**\n     * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link\n     * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise.\n     *\n     * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no\n     *     selection can be made otherwise.\n     * @return This builder.\n     */\n    public ParametersBuilder setExceedAudioConstraintsIfNecessary(\n        boolean exceedAudioConstraintsIfNecessary) {\n      this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary;\n      return this;\n    }\n\n    /**\n     * Sets whether to allow adaptive audio selections containing mixed MIME types.\n     *\n     * <p>Adaptations between different MIME types may not be completely seamless.\n     *\n     * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections\n     *     containing mixed MIME types.\n     * @return This builder.\n     */\n    public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness(\n        boolean allowAudioMixedMimeTypeAdaptiveness) {\n      this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness;\n      return this;\n    }\n\n    /**\n     * Sets whether to allow adaptive audio selections containing mixed sample rates.\n     *\n     * <p>Adaptations between different sample rates may not be completely seamless.\n     *\n     * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections\n     *     containing mixed sample rates.\n     * @return This builder.\n     */\n    public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness(\n        boolean allowAudioMixedSampleRateAdaptiveness) {\n      this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness;\n      return this;\n    }\n\n    /**\n     * Sets whether to allow adaptive audio selections containing mixed channel counts.\n     *\n     * <p>Adaptations between different channel counts may not be completely seamless.\n     *\n     * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections\n     *     containing mixed channel counts.\n     * @return This builder.\n     */\n    public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness(\n        boolean allowAudioMixedChannelCountAdaptiveness) {\n      this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness;\n      return this;\n    }\n\n    // Text\n\n    @Override\n    public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(\n        Context context) {\n      super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);\n      return this;\n    }\n\n    @Override\n    public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {\n      super.setPreferredTextLanguage(preferredTextLanguage);\n      return this;\n    }\n\n    @Override\n    public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {\n      super.setPreferredTextRoleFlags(preferredTextRoleFlags);\n      return this;\n    }\n\n    @Override\n    public ParametersBuilder setSelectUndeterminedTextLanguage(\n        boolean selectUndeterminedTextLanguage) {\n      super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);\n      return this;\n    }\n\n    @Override\n    public ParametersBuilder setDisabledTextTrackSelectionFlags(\n        @C.SelectionFlags int disabledTextTrackSelectionFlags) {\n      super.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags);\n      return this;\n    }\n\n    // General\n\n    /**\n     * Sets whether to force selection of the single lowest bitrate audio and video tracks that\n     * comply with all other constraints.\n     *\n     * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and\n     *     video tracks.\n     * @return This builder.\n     */\n    public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) {\n      this.forceLowestBitrate = forceLowestBitrate;\n      return this;\n    }\n\n    /**\n     * Sets whether to force selection of the highest bitrate audio and video tracks that comply\n     * with all other constraints.\n     *\n     * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio\n     *     and video tracks.\n     * @return This builder.\n     */\n    public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {\n      this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;\n      return this;\n    }\n\n    /**\n     * @deprecated Use {@link #setAllowVideoMixedMimeTypeAdaptiveness(boolean)} and {@link\n     *     #setAllowAudioMixedMimeTypeAdaptiveness(boolean)}.\n     */\n    @Deprecated\n    public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {\n      setAllowAudioMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness);\n      setAllowVideoMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness);\n      return this;\n    }\n\n    /** @deprecated Use {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} */\n    @Deprecated\n    public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {\n      return setAllowVideoNonSeamlessAdaptiveness(allowNonSeamlessAdaptiveness);\n    }\n\n    /**\n     * Sets whether to exceed renderer capabilities when no selection can be made otherwise.\n     *\n     * <p>This parameter applies when all of the tracks available for a renderer exceed the\n     * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality\n     * track will still be selected. Playback may succeed if the renderer has under-reported its\n     * true capabilities. If {@code false} then no track will be selected.\n     *\n     * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no\n     *     selection can be made otherwise.\n     * @return This builder.\n     */\n    public ParametersBuilder setExceedRendererCapabilitiesIfNecessary(\n        boolean exceedRendererCapabilitiesIfNecessary) {\n      this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;\n      return this;\n    }\n\n    /**\n     * Sets the audio session id to use when tunneling.\n     *\n     * <p>Enables or disables tunneling. To enable tunneling, pass an audio session id to use when\n     * in tunneling mode. Session ids can be generated using {@link\n     * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link\n     * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and\n     * supported by the audio and video renderers for the selected tracks.\n     *\n     * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link\n     *     C#AUDIO_SESSION_ID_UNSET} to disable tunneling.\n     * @return This builder.\n     */\n    public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) {\n      this.tunnelingAudioSessionId = tunnelingAudioSessionId;\n      return this;\n    }\n\n    // Overrides\n\n    /**\n     * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents\n     * the selector from selecting any tracks for it.\n     *\n     * @param rendererIndex The renderer index.\n     * @param disabled Whether the renderer is disabled.\n     * @return This builder.\n     */\n    public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) {\n      if (rendererDisabledFlags.get(rendererIndex) == disabled) {\n        // The disabled flag is unchanged.\n        return this;\n      }\n      // Only true values are placed in the array to make it easier to check for equality.\n      if (disabled) {\n        rendererDisabledFlags.put(rendererIndex, true);\n      } else {\n        rendererDisabledFlags.delete(rendererIndex);\n      }\n      return this;\n    }\n\n    /**\n     * Overrides the track selection for the renderer at the specified index.\n     *\n     * <p>When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the\n     * override is applied. When the {@link TrackGroupArray} does not match, the override has no\n     * effect. The override replaces any previous override for the specified {@link TrackGroupArray}\n     * for the specified {@link Renderer}.\n     *\n     * <p>Passing a {@code null} override will cause the renderer to be disabled when the {@link\n     * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} does\n     * not match a {@code null} override has no effect. Hence a {@code null} override differs from\n     * disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the renderer\n     * is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as {@link\n     * #setRendererDisabled(int, boolean)} disables the renderer unconditionally.\n     *\n     * <p>To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link\n     * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groups The {@link TrackGroupArray} for which the override should be applied.\n     * @param override The override.\n     * @return This builder.\n     */\n    public final ParametersBuilder setSelectionOverride(\n        int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) {\n      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n          selectionOverrides.get(rendererIndex);\n      if (overrides == null) {\n        overrides = new HashMap<>();\n        selectionOverrides.put(rendererIndex, overrides);\n      }\n      if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) {\n        // The override is unchanged.\n        return this;\n      }\n      overrides.put(groups, override);\n      return this;\n    }\n\n    /**\n     * Clears a track selection override for the specified renderer and {@link TrackGroupArray}.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groups The {@link TrackGroupArray} for which the override should be cleared.\n     * @return This builder.\n     */\n    public final ParametersBuilder clearSelectionOverride(\n        int rendererIndex, TrackGroupArray groups) {\n      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n          selectionOverrides.get(rendererIndex);\n      if (overrides == null || !overrides.containsKey(groups)) {\n        // Nothing to clear.\n        return this;\n      }\n      overrides.remove(groups);\n      if (overrides.isEmpty()) {\n        selectionOverrides.remove(rendererIndex);\n      }\n      return this;\n    }\n\n    /**\n     * Clears all track selection overrides for the specified renderer.\n     *\n     * @param rendererIndex The renderer index.\n     * @return This builder.\n     */\n    public final ParametersBuilder clearSelectionOverrides(int rendererIndex) {\n      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n          selectionOverrides.get(rendererIndex);\n      if (overrides == null || overrides.isEmpty()) {\n        // Nothing to clear.\n        return this;\n      }\n      selectionOverrides.remove(rendererIndex);\n      return this;\n    }\n\n    /**\n     * Clears all track selection overrides for all renderers.\n     *\n     * @return This builder.\n     */\n    public final ParametersBuilder clearSelectionOverrides() {\n      if (selectionOverrides.size() == 0) {\n        // Nothing to clear.\n        return this;\n      }\n      selectionOverrides.clear();\n      return this;\n    }\n\n    /**\n     * Builds a {@link Parameters} instance with the selected values.\n     */\n    public Parameters build() {\n      return new Parameters(\n          // Video\n          maxVideoWidth,\n          maxVideoHeight,\n          maxVideoFrameRate,\n          maxVideoBitrate,\n          exceedVideoConstraintsIfNecessary,\n          allowVideoMixedMimeTypeAdaptiveness,\n          allowVideoNonSeamlessAdaptiveness,\n          viewportWidth,\n          viewportHeight,\n          viewportOrientationMayChange,\n          // Audio\n          preferredAudioLanguage,\n          maxAudioChannelCount,\n          maxAudioBitrate,\n          exceedAudioConstraintsIfNecessary,\n          allowAudioMixedMimeTypeAdaptiveness,\n          allowAudioMixedSampleRateAdaptiveness,\n          allowAudioMixedChannelCountAdaptiveness,\n          // Text\n          preferredTextLanguage,\n          preferredTextRoleFlags,\n          selectUndeterminedTextLanguage,\n          disabledTextTrackSelectionFlags,\n          // General\n          forceLowestBitrate,\n          forceHighestSupportedBitrate,\n          exceedRendererCapabilitiesIfNecessary,\n          tunnelingAudioSessionId,\n          selectionOverrides,\n          rendererDisabledFlags);\n    }\n\n    private void setInitialValuesWithoutContext(@UnderInitialization ParametersBuilder this) {\n      // Video\n      maxVideoWidth = Integer.MAX_VALUE;\n      maxVideoHeight = Integer.MAX_VALUE;\n      maxVideoFrameRate = Integer.MAX_VALUE;\n      maxVideoBitrate = Integer.MAX_VALUE;\n      exceedVideoConstraintsIfNecessary = true;\n      allowVideoMixedMimeTypeAdaptiveness = false;\n      allowVideoNonSeamlessAdaptiveness = true;\n      viewportWidth = Integer.MAX_VALUE;\n      viewportHeight = Integer.MAX_VALUE;\n      viewportOrientationMayChange = true;\n      // Audio\n      maxAudioChannelCount = Integer.MAX_VALUE;\n      maxAudioBitrate = Integer.MAX_VALUE;\n      exceedAudioConstraintsIfNecessary = true;\n      allowAudioMixedMimeTypeAdaptiveness = false;\n      allowAudioMixedSampleRateAdaptiveness = false;\n      allowAudioMixedChannelCountAdaptiveness = false;\n      // General\n      forceLowestBitrate = false;\n      forceHighestSupportedBitrate = false;\n      exceedRendererCapabilitiesIfNecessary = true;\n      tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;\n    }\n\n    private static SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>\n        cloneSelectionOverrides(\n            SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {\n      SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> clone =\n          new SparseArray<>();\n      for (int i = 0; i < selectionOverrides.size(); i++) {\n        clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i)));\n      }\n      return clone;\n    }\n  }\n\n  /**\n   * Extends {@link TrackSelectionParameters} by adding fields that are specific to {@link\n   * DefaultTrackSelector}.\n   */\n  public static final class Parameters extends TrackSelectionParameters {\n\n    /**\n     * An instance with default values, except those obtained from the {@link Context}.\n     *\n     * <p>If possible, use {@link #getDefaults(Context)} instead.\n     *\n     * <p>This instance will not have the following settings:\n     *\n     * <ul>\n     *   <li>{@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean)\n     *       Viewport constraints} configured for the primary display.\n     *   <li>{@link\n     *       ParametersBuilder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)\n     *       Preferred text language and role flags} configured to the accessibility settings of\n     *       {@link android.view.accessibility.CaptioningManager}.\n     * </ul>\n     */\n    @SuppressWarnings(\"deprecation\")\n    public static final Parameters DEFAULT_WITHOUT_CONTEXT = new ParametersBuilder().build();\n\n    /**\n     * @deprecated This instance does not have {@link Context} constraints configured. Use {@link\n     *     #getDefaults(Context)} instead.\n     */\n    @Deprecated public static final Parameters DEFAULT_WITHOUT_VIEWPORT = DEFAULT_WITHOUT_CONTEXT;\n\n    /**\n     * @deprecated This instance does not have {@link Context} constraints configured. Use {@link\n     *     #getDefaults(Context)} instead.\n     */\n    @Deprecated\n    public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;\n\n    /** Returns an instance configured with default values. */\n    public static Parameters getDefaults(Context context) {\n      return new ParametersBuilder(context).build();\n    }\n\n    // Video\n    /**\n     * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e.\n     * no constraint).\n     *\n     * <p>To constrain adaptive video track selections to be suitable for a given viewport (the\n     * region of the display within which video will be played), use ({@link #viewportWidth}, {@link\n     * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.\n     */\n    public final int maxVideoWidth;\n    /**\n     * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e.\n     * no constraint).\n     *\n     * <p>To constrain adaptive video track selections to be suitable for a given viewport (the\n     * region of the display within which video will be played), use ({@link #viewportWidth}, {@link\n     * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.\n     */\n    public final int maxVideoHeight;\n    /**\n     * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE}\n     * (i.e. no constraint).\n     */\n    public final int maxVideoFrameRate;\n    /**\n     * Maximum allowed video bitrate in bits per second. The default value is {@link\n     * Integer#MAX_VALUE} (i.e. no constraint).\n     */\n    public final int maxVideoBitrate;\n    /**\n     * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link\n     * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is\n     * {@code true}.\n     */\n    public final boolean exceedVideoConstraintsIfNecessary;\n    /**\n     * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between\n     * different MIME types may not be completely seamless, in which case {@link\n     * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type\n     * selections to be made. The default value is {@code false}.\n     */\n    public final boolean allowVideoMixedMimeTypeAdaptiveness;\n    /**\n     * Whether to allow adaptive video selections where adaptation may not be completely seamless.\n     * The default value is {@code true}.\n     */\n    public final boolean allowVideoNonSeamlessAdaptiveness;\n    /**\n     * Viewport width in pixels. Constrains video track selections for adaptive content so that only\n     * tracks suitable for the viewport are selected. The default value is the physical width of the\n     * primary display, in pixels.\n     */\n    public final int viewportWidth;\n    /**\n     * Viewport height in pixels. Constrains video track selections for adaptive content so that\n     * only tracks suitable for the viewport are selected. The default value is the physical height\n     * of the primary display, in pixels.\n     */\n    public final int viewportHeight;\n    /**\n     * Whether the viewport orientation may change during playback. Constrains video track\n     * selections for adaptive content so that only tracks suitable for the viewport are selected.\n     * The default value is {@code true}.\n     */\n    public final boolean viewportOrientationMayChange;\n    // Audio\n    /**\n     * Maximum allowed audio channel count. The default value is {@link Integer#MAX_VALUE} (i.e. no\n     * constraint).\n     */\n    public final int maxAudioChannelCount;\n    /**\n     * Maximum allowed audio bitrate in bits per second. The default value is {@link\n     * Integer#MAX_VALUE} (i.e. no constraint).\n     */\n    public final int maxAudioBitrate;\n    /**\n     * Whether to exceed the {@link #maxAudioChannelCount} and {@link #maxAudioBitrate} constraints\n     * when no selection can be made otherwise. The default value is {@code true}.\n     */\n    public final boolean exceedAudioConstraintsIfNecessary;\n    /**\n     * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between\n     * different MIME types may not be completely seamless. The default value is {@code false}.\n     */\n    public final boolean allowAudioMixedMimeTypeAdaptiveness;\n    /**\n     * Whether to allow adaptive audio selections containing mixed sample rates. Adaptations between\n     * different sample rates may not be completely seamless. The default value is {@code false}.\n     */\n    public final boolean allowAudioMixedSampleRateAdaptiveness;\n    /**\n     * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations\n     * between different channel counts may not be completely seamless. The default value is {@code\n     * false}.\n     */\n    public final boolean allowAudioMixedChannelCountAdaptiveness;\n\n    // General\n    /**\n     * Whether to force selection of the single lowest bitrate audio and video tracks that comply\n     * with all other constraints. The default value is {@code false}.\n     */\n    public final boolean forceLowestBitrate;\n    /**\n     * Whether to force selection of the highest bitrate audio and video tracks that comply with all\n     * other constraints. The default value is {@code false}.\n     */\n    public final boolean forceHighestSupportedBitrate;\n    /**\n     * @deprecated Use {@link #allowVideoMixedMimeTypeAdaptiveness} and {@link\n     *     #allowAudioMixedMimeTypeAdaptiveness}.\n     */\n    @Deprecated public final boolean allowMixedMimeAdaptiveness;\n    /** @deprecated Use {@link #allowVideoNonSeamlessAdaptiveness}. */\n    @Deprecated public final boolean allowNonSeamlessAdaptiveness;\n    /**\n     * Whether to exceed renderer capabilities when no selection can be made otherwise.\n     *\n     * <p>This parameter applies when all of the tracks available for a renderer exceed the\n     * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality\n     * track will still be selected. Playback may succeed if the renderer has under-reported its\n     * true capabilities. If {@code false} then no track will be selected. The default value is\n     * {@code true}.\n     */\n    public final boolean exceedRendererCapabilitiesIfNecessary;\n    /**\n     * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling\n     * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is\n     * disabled).\n     */\n    public final int tunnelingAudioSessionId;\n\n    // Overrides\n    private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>\n        selectionOverrides;\n    private final SparseBooleanArray rendererDisabledFlags;\n\n    /* package */ Parameters(\n        // Video\n        int maxVideoWidth,\n        int maxVideoHeight,\n        int maxVideoFrameRate,\n        int maxVideoBitrate,\n        boolean exceedVideoConstraintsIfNecessary,\n        boolean allowVideoMixedMimeTypeAdaptiveness,\n        boolean allowVideoNonSeamlessAdaptiveness,\n        int viewportWidth,\n        int viewportHeight,\n        boolean viewportOrientationMayChange,\n        // Audio\n        @Nullable String preferredAudioLanguage,\n        int maxAudioChannelCount,\n        int maxAudioBitrate,\n        boolean exceedAudioConstraintsIfNecessary,\n        boolean allowAudioMixedMimeTypeAdaptiveness,\n        boolean allowAudioMixedSampleRateAdaptiveness,\n        boolean allowAudioMixedChannelCountAdaptiveness,\n        // Text\n        @Nullable String preferredTextLanguage,\n        @C.RoleFlags int preferredTextRoleFlags,\n        boolean selectUndeterminedTextLanguage,\n        @C.SelectionFlags int disabledTextTrackSelectionFlags,\n        // General\n        boolean forceLowestBitrate,\n        boolean forceHighestSupportedBitrate,\n        boolean exceedRendererCapabilitiesIfNecessary,\n        int tunnelingAudioSessionId,\n        // Overrides\n        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides,\n        SparseBooleanArray rendererDisabledFlags) {\n      super(\n          preferredAudioLanguage,\n          preferredTextLanguage,\n          preferredTextRoleFlags,\n          selectUndeterminedTextLanguage,\n          disabledTextTrackSelectionFlags);\n      // Video\n      this.maxVideoWidth = maxVideoWidth;\n      this.maxVideoHeight = maxVideoHeight;\n      this.maxVideoFrameRate = maxVideoFrameRate;\n      this.maxVideoBitrate = maxVideoBitrate;\n      this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;\n      this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;\n      this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;\n      this.viewportWidth = viewportWidth;\n      this.viewportHeight = viewportHeight;\n      this.viewportOrientationMayChange = viewportOrientationMayChange;\n      // Audio\n      this.maxAudioChannelCount = maxAudioChannelCount;\n      this.maxAudioBitrate = maxAudioBitrate;\n      this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary;\n      this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness;\n      this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness;\n      this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness;\n      // General\n      this.forceLowestBitrate = forceLowestBitrate;\n      this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;\n      this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;\n      this.tunnelingAudioSessionId = tunnelingAudioSessionId;\n      // Deprecated fields.\n      this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;\n      this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;\n      // Overrides\n      this.selectionOverrides = selectionOverrides;\n      this.rendererDisabledFlags = rendererDisabledFlags;\n    }\n\n    /* package */\n    Parameters(Parcel in) {\n      super(in);\n      // Video\n      this.maxVideoWidth = in.readInt();\n      this.maxVideoHeight = in.readInt();\n      this.maxVideoFrameRate = in.readInt();\n      this.maxVideoBitrate = in.readInt();\n      this.exceedVideoConstraintsIfNecessary = Util.readBoolean(in);\n      this.allowVideoMixedMimeTypeAdaptiveness = Util.readBoolean(in);\n      this.allowVideoNonSeamlessAdaptiveness = Util.readBoolean(in);\n      this.viewportWidth = in.readInt();\n      this.viewportHeight = in.readInt();\n      this.viewportOrientationMayChange = Util.readBoolean(in);\n      // Audio\n      this.maxAudioChannelCount = in.readInt();\n      this.maxAudioBitrate = in.readInt();\n      this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in);\n      this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in);\n      this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in);\n      this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in);\n      // General\n      this.forceLowestBitrate = Util.readBoolean(in);\n      this.forceHighestSupportedBitrate = Util.readBoolean(in);\n      this.exceedRendererCapabilitiesIfNecessary = Util.readBoolean(in);\n      this.tunnelingAudioSessionId = in.readInt();\n      // Overrides\n      this.selectionOverrides = readSelectionOverrides(in);\n      this.rendererDisabledFlags = Util.castNonNull(in.readSparseBooleanArray());\n      // Deprecated fields.\n      this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;\n      this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;\n    }\n\n    /**\n     * Returns whether the renderer is disabled.\n     *\n     * @param rendererIndex The renderer index.\n     * @return Whether the renderer is disabled.\n     */\n    public final boolean getRendererDisabled(int rendererIndex) {\n      return rendererDisabledFlags.get(rendererIndex);\n    }\n\n    /**\n     * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groups The {@link TrackGroupArray}.\n     * @return Whether there is an override.\n     */\n    public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {\n      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n          selectionOverrides.get(rendererIndex);\n      return overrides != null && overrides.containsKey(groups);\n    }\n\n    /**\n     * Returns the override for the specified renderer and {@link TrackGroupArray}.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groups The {@link TrackGroupArray}.\n     * @return The override, or null if no override exists.\n     */\n    @Nullable\n    public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {\n      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n          selectionOverrides.get(rendererIndex);\n      return overrides != null ? overrides.get(groups) : null;\n    }\n\n    /** Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */\n    @Override\n    public ParametersBuilder buildUpon() {\n      return new ParametersBuilder(this);\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      Parameters other = (Parameters) obj;\n      return super.equals(obj)\n          // Video\n          && maxVideoWidth == other.maxVideoWidth\n          && maxVideoHeight == other.maxVideoHeight\n          && maxVideoFrameRate == other.maxVideoFrameRate\n          && maxVideoBitrate == other.maxVideoBitrate\n          && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary\n          && allowVideoMixedMimeTypeAdaptiveness == other.allowVideoMixedMimeTypeAdaptiveness\n          && allowVideoNonSeamlessAdaptiveness == other.allowVideoNonSeamlessAdaptiveness\n          && viewportOrientationMayChange == other.viewportOrientationMayChange\n          && viewportWidth == other.viewportWidth\n          && viewportHeight == other.viewportHeight\n          // Audio\n          && maxAudioChannelCount == other.maxAudioChannelCount\n          && maxAudioBitrate == other.maxAudioBitrate\n          && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary\n          && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness\n          && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness\n          && allowAudioMixedChannelCountAdaptiveness\n              == other.allowAudioMixedChannelCountAdaptiveness\n          // General\n          && forceLowestBitrate == other.forceLowestBitrate\n          && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate\n          && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary\n          && tunnelingAudioSessionId == other.tunnelingAudioSessionId\n          // Overrides\n          && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags)\n          && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = super.hashCode();\n      // Video\n      result = 31 * result + maxVideoWidth;\n      result = 31 * result + maxVideoHeight;\n      result = 31 * result + maxVideoFrameRate;\n      result = 31 * result + maxVideoBitrate;\n      result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);\n      result = 31 * result + (allowVideoMixedMimeTypeAdaptiveness ? 1 : 0);\n      result = 31 * result + (allowVideoNonSeamlessAdaptiveness ? 1 : 0);\n      result = 31 * result + (viewportOrientationMayChange ? 1 : 0);\n      result = 31 * result + viewportWidth;\n      result = 31 * result + viewportHeight;\n      // Audio\n      result = 31 * result + maxAudioChannelCount;\n      result = 31 * result + maxAudioBitrate;\n      result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0);\n      result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0);\n      result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0);\n      result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0);\n      // General\n      result = 31 * result + (forceLowestBitrate ? 1 : 0);\n      result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0);\n      result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);\n      result = 31 * result + tunnelingAudioSessionId;\n      // Overrides (omitted from hashCode).\n      return result;\n    }\n\n    // Parcelable implementation.\n\n    @Override\n    public int describeContents() {\n      return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n      super.writeToParcel(dest, flags);\n      // Video\n      dest.writeInt(maxVideoWidth);\n      dest.writeInt(maxVideoHeight);\n      dest.writeInt(maxVideoFrameRate);\n      dest.writeInt(maxVideoBitrate);\n      Util.writeBoolean(dest, exceedVideoConstraintsIfNecessary);\n      Util.writeBoolean(dest, allowVideoMixedMimeTypeAdaptiveness);\n      Util.writeBoolean(dest, allowVideoNonSeamlessAdaptiveness);\n      dest.writeInt(viewportWidth);\n      dest.writeInt(viewportHeight);\n      Util.writeBoolean(dest, viewportOrientationMayChange);\n      // Audio\n      dest.writeInt(maxAudioChannelCount);\n      dest.writeInt(maxAudioBitrate);\n      Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary);\n      Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness);\n      Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness);\n      Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness);\n      // General\n      Util.writeBoolean(dest, forceLowestBitrate);\n      Util.writeBoolean(dest, forceHighestSupportedBitrate);\n      Util.writeBoolean(dest, exceedRendererCapabilitiesIfNecessary);\n      dest.writeInt(tunnelingAudioSessionId);\n      // Overrides\n      writeSelectionOverridesToParcel(dest, selectionOverrides);\n      dest.writeSparseBooleanArray(rendererDisabledFlags);\n    }\n\n    public static final Parcelable.Creator<Parameters> CREATOR =\n        new Parcelable.Creator<Parameters>() {\n\n          @Override\n          public Parameters createFromParcel(Parcel in) {\n            return new Parameters(in);\n          }\n\n          @Override\n          public Parameters[] newArray(int size) {\n            return new Parameters[size];\n          }\n        };\n\n    // Static utility methods.\n\n    private static SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>\n        readSelectionOverrides(Parcel in) {\n      int renderersWithOverridesCount = in.readInt();\n      SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides =\n          new SparseArray<>(renderersWithOverridesCount);\n      for (int i = 0; i < renderersWithOverridesCount; i++) {\n        int rendererIndex = in.readInt();\n        int overrideCount = in.readInt();\n        Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n            new HashMap<>(overrideCount);\n        for (int j = 0; j < overrideCount; j++) {\n          TrackGroupArray trackGroups =\n              Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader()));\n          @Nullable\n          SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader());\n          overrides.put(trackGroups, override);\n        }\n        selectionOverrides.put(rendererIndex, overrides);\n      }\n      return selectionOverrides;\n    }\n\n    private static void writeSelectionOverridesToParcel(\n        Parcel dest,\n        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {\n      int renderersWithOverridesCount = selectionOverrides.size();\n      dest.writeInt(renderersWithOverridesCount);\n      for (int i = 0; i < renderersWithOverridesCount; i++) {\n        int rendererIndex = selectionOverrides.keyAt(i);\n        Map<TrackGroupArray, @NullableType SelectionOverride> overrides =\n            selectionOverrides.valueAt(i);\n        int overrideCount = overrides.size();\n        dest.writeInt(rendererIndex);\n        dest.writeInt(overrideCount);\n        for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> override :\n            overrides.entrySet()) {\n          dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0);\n          dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0);\n        }\n      }\n    }\n\n    private static boolean areRendererDisabledFlagsEqual(\n        SparseBooleanArray first, SparseBooleanArray second) {\n      int firstSize = first.size();\n      if (second.size() != firstSize) {\n        return false;\n      }\n      // Only true values are put into rendererDisabledFlags, so we don't need to compare values.\n      for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) {\n        if (second.indexOfKey(first.keyAt(indexInFirst)) < 0) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    private static boolean areSelectionOverridesEqual(\n        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> first,\n        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> second) {\n      int firstSize = first.size();\n      if (second.size() != firstSize) {\n        return false;\n      }\n      for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) {\n        int indexInSecond = second.indexOfKey(first.keyAt(indexInFirst));\n        if (indexInSecond < 0\n            || !areSelectionOverridesEqual(\n                first.valueAt(indexInFirst), second.valueAt(indexInSecond))) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    private static boolean areSelectionOverridesEqual(\n        Map<TrackGroupArray, @NullableType SelectionOverride> first,\n        Map<TrackGroupArray, @NullableType SelectionOverride> second) {\n      int firstSize = first.size();\n      if (second.size() != firstSize) {\n        return false;\n      }\n      for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> firstEntry :\n          first.entrySet()) {\n        TrackGroupArray key = firstEntry.getKey();\n        if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) {\n          return false;\n        }\n      }\n      return true;\n    }\n  }\n\n  /** A track selection override. */\n  public static final class SelectionOverride implements Parcelable {\n\n    public final int groupIndex;\n    public final int[] tracks;\n    public final int length;\n    public final int reason;\n    public final int data;\n\n    /**\n     * @param groupIndex The overriding track group index.\n     * @param tracks The overriding track indices within the track group.\n     */\n    public SelectionOverride(int groupIndex, int... tracks) {\n      this(groupIndex, tracks, C.SELECTION_REASON_MANUAL, /* data= */ 0);\n    }\n\n    /**\n     * @param groupIndex The overriding track group index.\n     * @param tracks The overriding track indices within the track group.\n     * @param reason The reason for the override. One of the {@link C} SELECTION_REASON_ constants.\n     * @param data Optional data associated with this override.\n     */\n    public SelectionOverride(int groupIndex, int[] tracks, int reason, int data) {\n      this.groupIndex = groupIndex;\n      this.tracks = Arrays.copyOf(tracks, tracks.length);\n      this.length = tracks.length;\n      this.reason = reason;\n      this.data = data;\n      Arrays.sort(this.tracks);\n    }\n\n    /* package */ SelectionOverride(Parcel in) {\n      groupIndex = in.readInt();\n      length = in.readByte();\n      tracks = new int[length];\n      in.readIntArray(tracks);\n      reason = in.readInt();\n      data = in.readInt();\n    }\n\n    /** Returns whether this override contains the specified track index. */\n    public boolean containsTrack(int track) {\n      for (int overrideTrack : tracks) {\n        if (overrideTrack == track) {\n          return true;\n        }\n      }\n      return false;\n    }\n\n    @Override\n    public int hashCode() {\n      int hash = 31 * groupIndex + Arrays.hashCode(tracks);\n      hash = 31 * hash + reason;\n      return 31 * hash + data;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      SelectionOverride other = (SelectionOverride) obj;\n      return groupIndex == other.groupIndex\n          && Arrays.equals(tracks, other.tracks)\n          && reason == other.reason\n          && data == other.data;\n    }\n\n    // Parcelable implementation.\n\n    @Override\n    public int describeContents() {\n      return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n      dest.writeInt(groupIndex);\n      dest.writeInt(tracks.length);\n      dest.writeIntArray(tracks);\n      dest.writeInt(reason);\n      dest.writeInt(data);\n    }\n\n    public static final Creator<SelectionOverride> CREATOR =\n        new Creator<SelectionOverride>() {\n\n          @Override\n          public SelectionOverride createFromParcel(Parcel in) {\n            return new SelectionOverride(in);\n          }\n\n          @Override\n          public SelectionOverride[] newArray(int size) {\n            return new SelectionOverride[size];\n          }\n        };\n  }\n\n  /**\n   * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the\n   * corresponding viewport dimension, then the video is considered as filling the viewport (in that\n   * dimension).\n   */\n  private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;\n  private static final int[] NO_TRACKS = new int[0];\n  private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;\n\n  private final TrackSelection.Factory trackSelectionFactory;\n  private final AtomicReference<Parameters> parametersReference;\n\n  private boolean allowMultipleAdaptiveSelections;\n\n  /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultTrackSelector() {\n    this(new AdaptiveTrackSelection.Factory());\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be\n   *     passed directly to the player in {@link\n   *     com.google.android.exoplayer2.SimpleExoPlayer.Builder}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {\n    this(new AdaptiveTrackSelection.Factory(bandwidthMeter));\n  }\n\n  /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */\n  @Deprecated\n  public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) {\n    this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory);\n  }\n\n  /** @param context Any {@link Context}. */\n  public DefaultTrackSelector(Context context) {\n    this(context, new AdaptiveTrackSelection.Factory());\n  }\n\n  /**\n   * @param context Any {@link Context}.\n   * @param trackSelectionFactory A factory for {@link TrackSelection}s.\n   */\n  public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) {\n    this(Parameters.getDefaults(context), trackSelectionFactory);\n  }\n\n  /**\n   * @param parameters Initial {@link Parameters}.\n   * @param trackSelectionFactory A factory for {@link TrackSelection}s.\n   */\n  public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) {\n    this.trackSelectionFactory = trackSelectionFactory;\n    parametersReference = new AtomicReference<>(parameters);\n  }\n\n  /**\n   * Atomically sets the provided parameters for track selection.\n   *\n   * @param parameters The parameters for track selection.\n   */\n  public void setParameters(Parameters parameters) {\n    Assertions.checkNotNull(parameters);\n    if (!parametersReference.getAndSet(parameters).equals(parameters)) {\n      invalidate();\n    }\n  }\n\n  /**\n   * Atomically sets the provided parameters for track selection.\n   *\n   * @param parametersBuilder A builder from which to obtain the parameters for track selection.\n   */\n  public void setParameters(ParametersBuilder parametersBuilder) {\n    setParameters(parametersBuilder.build());\n  }\n\n  /**\n   * Gets the current selection parameters.\n   *\n   * @return The current selection parameters.\n   */\n  public Parameters getParameters() {\n    return parametersReference.get();\n  }\n\n  /** Returns a new {@link ParametersBuilder} initialized with the current selection parameters. */\n  public ParametersBuilder buildUponParameters() {\n    return getParameters().buildUpon();\n  }\n\n  /** @deprecated Use {@link ParametersBuilder#setRendererDisabled(int, boolean)}. */\n  @Deprecated\n  public final void setRendererDisabled(int rendererIndex, boolean disabled) {\n    setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled));\n  }\n\n  /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. */\n  @Deprecated\n  public final boolean getRendererDisabled(int rendererIndex) {\n    return getParameters().getRendererDisabled(rendererIndex);\n  }\n\n  /**\n   * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray,\n   *     SelectionOverride)}.\n   */\n  @Deprecated\n  public final void setSelectionOverride(\n      int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) {\n    setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override));\n  }\n\n  /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. */\n  @Deprecated\n  public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {\n    return getParameters().hasSelectionOverride(rendererIndex, groups);\n  }\n\n  /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */\n  @Deprecated\n  @Nullable\n  public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {\n    return getParameters().getSelectionOverride(rendererIndex, groups);\n  }\n\n  /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */\n  @Deprecated\n  public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {\n    setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups));\n  }\n\n  /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */\n  @Deprecated\n  public final void clearSelectionOverrides(int rendererIndex) {\n    setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex));\n  }\n\n  /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */\n  @Deprecated\n  public final void clearSelectionOverrides() {\n    setParameters(buildUponParameters().clearSelectionOverrides());\n  }\n\n  /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */\n  @Deprecated\n  public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) {\n    setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId));\n  }\n\n  /**\n   * Allows the creation of multiple adaptive track selections.\n   *\n   * <p>This method is experimental, and will be renamed or removed in a future release.\n   */\n  public void experimental_allowMultipleAdaptiveSelections() {\n    this.allowMultipleAdaptiveSelections = true;\n  }\n\n  // MappingTrackSelector implementation.\n\n  @Override\n  protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>\n      selectTracks(\n          MappedTrackInfo mappedTrackInfo,\n          @Capabilities int[][][] rendererFormatSupports,\n          @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports)\n          throws ExoPlaybackException {\n    Parameters params = parametersReference.get();\n    int rendererCount = mappedTrackInfo.getRendererCount();\n    TrackSelection.@NullableType Definition[] definitions =\n        selectAllTracks(\n            mappedTrackInfo,\n            rendererFormatSupports,\n            rendererMixedMimeTypeAdaptationSupports,\n            params);\n\n    // Apply track disabling and overriding.\n    for (int i = 0; i < rendererCount; i++) {\n      if (params.getRendererDisabled(i)) {\n        definitions[i] = null;\n        continue;\n      }\n      TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(i);\n      if (params.hasSelectionOverride(i, rendererTrackGroups)) {\n        SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups);\n        definitions[i] =\n            override == null\n                ? null\n                : new TrackSelection.Definition(\n                    rendererTrackGroups.get(override.groupIndex),\n                    override.tracks,\n                    override.reason,\n                    override.data);\n      }\n    }\n\n    @NullableType\n    TrackSelection[] rendererTrackSelections =\n        trackSelectionFactory.createTrackSelections(definitions, getBandwidthMeter());\n\n    // Initialize the renderer configurations to the default configuration for all renderers with\n    // selections, and null otherwise.\n    @NullableType RendererConfiguration[] rendererConfigurations =\n        new RendererConfiguration[rendererCount];\n    for (int i = 0; i < rendererCount; i++) {\n      boolean forceRendererDisabled = params.getRendererDisabled(i);\n      boolean rendererEnabled =\n          !forceRendererDisabled\n              && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE\n                  || rendererTrackSelections[i] != null);\n      rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null;\n    }\n\n    // Configure audio and video renderers to use tunneling if appropriate.\n    maybeConfigureRenderersForTunneling(\n        mappedTrackInfo,\n        rendererFormatSupports,\n        rendererConfigurations,\n        rendererTrackSelections,\n        params.tunnelingAudioSessionId);\n\n    return Pair.create(rendererConfigurations, rendererTrackSelections);\n  }\n\n  // Track selection prior to overrides and disabled flags being applied.\n\n  /**\n   * Called from {@link #selectTracks(MappedTrackInfo, int[][][], int[])} to make a track selection\n   * for each renderer, prior to overrides and disabled flags being applied.\n   *\n   * <p>The implementation should not account for overrides and disabled flags. Track selections\n   * generated by this method will be overridden to account for these properties.\n   *\n   * @param mappedTrackInfo Mapped track information.\n   * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by\n   *     renderer, track group and track (in that order).\n   * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type\n   *     adaptation for the renderer.\n   * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no\n   *     selection was made.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  protected TrackSelection.@NullableType Definition[] selectAllTracks(\n      MappedTrackInfo mappedTrackInfo,\n      @Capabilities int[][][] rendererFormatSupports,\n      @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,\n      Parameters params)\n      throws ExoPlaybackException {\n    int rendererCount = mappedTrackInfo.getRendererCount();\n    TrackSelection.@NullableType Definition[] definitions =\n        new TrackSelection.Definition[rendererCount];\n\n    boolean seenVideoRendererWithMappedTracks = false;\n    boolean selectedVideoTracks = false;\n    for (int i = 0; i < rendererCount; i++) {\n      if (C.TRACK_TYPE_VIDEO == mappedTrackInfo.getRendererType(i)) {\n        if (!selectedVideoTracks) {\n          definitions[i] =\n              selectVideoTrack(\n                  mappedTrackInfo.getTrackGroups(i),\n                  rendererFormatSupports[i],\n                  rendererMixedMimeTypeAdaptationSupports[i],\n                  params,\n                  /* enableAdaptiveTrackSelection= */ true);\n          selectedVideoTracks = definitions[i] != null;\n        }\n        seenVideoRendererWithMappedTracks |= mappedTrackInfo.getTrackGroups(i).length > 0;\n      }\n    }\n\n    AudioTrackScore selectedAudioTrackScore = null;\n    String selectedAudioLanguage = null;\n    int selectedAudioRendererIndex = C.INDEX_UNSET;\n    for (int i = 0; i < rendererCount; i++) {\n      if (C.TRACK_TYPE_AUDIO == mappedTrackInfo.getRendererType(i)) {\n        boolean enableAdaptiveTrackSelection =\n            allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks;\n        Pair<TrackSelection.Definition, AudioTrackScore> audioSelection =\n            selectAudioTrack(\n                mappedTrackInfo.getTrackGroups(i),\n                rendererFormatSupports[i],\n                rendererMixedMimeTypeAdaptationSupports[i],\n                params,\n                enableAdaptiveTrackSelection);\n        if (audioSelection != null\n            && (selectedAudioTrackScore == null\n                || audioSelection.second.compareTo(selectedAudioTrackScore) > 0)) {\n          if (selectedAudioRendererIndex != C.INDEX_UNSET) {\n            // We've already made a selection for another audio renderer, but it had a lower\n            // score. Clear the selection for that renderer.\n            definitions[selectedAudioRendererIndex] = null;\n          }\n          TrackSelection.Definition definition = audioSelection.first;\n          definitions[i] = definition;\n          // We assume that audio tracks in the same group have matching language.\n          selectedAudioLanguage = definition.group.getFormat(definition.tracks[0]).language;\n          selectedAudioTrackScore = audioSelection.second;\n          selectedAudioRendererIndex = i;\n        }\n      }\n    }\n\n    TextTrackScore selectedTextTrackScore = null;\n    int selectedTextRendererIndex = C.INDEX_UNSET;\n    for (int i = 0; i < rendererCount; i++) {\n      int trackType = mappedTrackInfo.getRendererType(i);\n      switch (trackType) {\n        case C.TRACK_TYPE_VIDEO:\n        case C.TRACK_TYPE_AUDIO:\n          // Already done. Do nothing.\n          break;\n        case C.TRACK_TYPE_TEXT:\n          Pair<TrackSelection.Definition, TextTrackScore> textSelection =\n              selectTextTrack(\n                  mappedTrackInfo.getTrackGroups(i),\n                  rendererFormatSupports[i],\n                  params,\n                  selectedAudioLanguage);\n          if (textSelection != null\n              && (selectedTextTrackScore == null\n                  || textSelection.second.compareTo(selectedTextTrackScore) > 0)) {\n            if (selectedTextRendererIndex != C.INDEX_UNSET) {\n              // We've already made a selection for another text renderer, but it had a lower score.\n              // Clear the selection for that renderer.\n              definitions[selectedTextRendererIndex] = null;\n            }\n            definitions[i] = textSelection.first;\n            selectedTextTrackScore = textSelection.second;\n            selectedTextRendererIndex = i;\n          }\n          break;\n        default:\n          definitions[i] =\n              selectOtherTrack(\n                  trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params);\n          break;\n      }\n    }\n\n    return definitions;\n  }\n\n  // Video track selection implementation.\n\n  /**\n   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a\n   * {@link TrackSelection} for a video renderer.\n   *\n   * @param groups The {@link TrackGroupArray} mapped to the renderer.\n   * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer,\n   *     track group and track (in that order).\n   * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type\n   *     adaptation for the renderer.\n   * @param params The selector's current constraint parameters.\n   * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.\n   * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was\n   *     made.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  @Nullable\n  protected TrackSelection.Definition selectVideoTrack(\n      TrackGroupArray groups,\n      @Capabilities int[][] formatSupports,\n      @AdaptiveSupport int mixedMimeTypeAdaptationSupports,\n      Parameters params,\n      boolean enableAdaptiveTrackSelection)\n      throws ExoPlaybackException {\n    TrackSelection.Definition definition = null;\n    if (!params.forceHighestSupportedBitrate\n        && !params.forceLowestBitrate\n        && enableAdaptiveTrackSelection) {\n      definition =\n          selectAdaptiveVideoTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params);\n    }\n    if (definition == null) {\n      definition = selectFixedVideoTrack(groups, formatSupports, params);\n    }\n    return definition;\n  }\n\n  @Nullable\n  private static TrackSelection.Definition selectAdaptiveVideoTrack(\n      TrackGroupArray groups,\n      @Capabilities int[][] formatSupport,\n      @AdaptiveSupport int mixedMimeTypeAdaptationSupports,\n      Parameters params) {\n    int requiredAdaptiveSupport =\n        params.allowVideoNonSeamlessAdaptiveness\n            ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)\n            : RendererCapabilities.ADAPTIVE_SEAMLESS;\n    boolean allowMixedMimeTypes =\n        params.allowVideoMixedMimeTypeAdaptiveness\n            && (mixedMimeTypeAdaptationSupports & requiredAdaptiveSupport) != 0;\n    for (int i = 0; i < groups.length; i++) {\n      TrackGroup group = groups.get(i);\n      int[] adaptiveTracks =\n          getAdaptiveVideoTracksForGroup(\n              group,\n              formatSupport[i],\n              allowMixedMimeTypes,\n              requiredAdaptiveSupport,\n              params.maxVideoWidth,\n              params.maxVideoHeight,\n              params.maxVideoFrameRate,\n              params.maxVideoBitrate,\n              params.viewportWidth,\n              params.viewportHeight,\n              params.viewportOrientationMayChange);\n      if (adaptiveTracks.length > 0) {\n        return new TrackSelection.Definition(group, adaptiveTracks);\n      }\n    }\n    return null;\n  }\n\n  private static int[] getAdaptiveVideoTracksForGroup(\n      TrackGroup group,\n      @Capabilities int[] formatSupport,\n      boolean allowMixedMimeTypes,\n      int requiredAdaptiveSupport,\n      int maxVideoWidth,\n      int maxVideoHeight,\n      int maxVideoFrameRate,\n      int maxVideoBitrate,\n      int viewportWidth,\n      int viewportHeight,\n      boolean viewportOrientationMayChange) {\n    if (group.length < 2) {\n      return NO_TRACKS;\n    }\n\n    List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,\n        viewportHeight, viewportOrientationMayChange);\n    if (selectedTrackIndices.size() < 2) {\n      return NO_TRACKS;\n    }\n\n    String selectedMimeType = null;\n    if (!allowMixedMimeTypes) {\n      // Select the mime type for which we have the most adaptive tracks.\n      HashSet<@NullableType String> seenMimeTypes = new HashSet<>();\n      int selectedMimeTypeTrackCount = 0;\n      for (int i = 0; i < selectedTrackIndices.size(); i++) {\n        int trackIndex = selectedTrackIndices.get(i);\n        String sampleMimeType = group.getFormat(trackIndex).sampleMimeType;\n        if (seenMimeTypes.add(sampleMimeType)) {\n          int countForMimeType =\n              getAdaptiveVideoTrackCountForMimeType(\n                  group,\n                  formatSupport,\n                  requiredAdaptiveSupport,\n                  sampleMimeType,\n                  maxVideoWidth,\n                  maxVideoHeight,\n                  maxVideoFrameRate,\n                  maxVideoBitrate,\n                  selectedTrackIndices);\n          if (countForMimeType > selectedMimeTypeTrackCount) {\n            selectedMimeType = sampleMimeType;\n            selectedMimeTypeTrackCount = countForMimeType;\n          }\n        }\n      }\n    }\n\n    // Filter by the selected mime type.\n    filterAdaptiveVideoTrackCountForMimeType(\n        group,\n        formatSupport,\n        requiredAdaptiveSupport,\n        selectedMimeType,\n        maxVideoWidth,\n        maxVideoHeight,\n        maxVideoFrameRate,\n        maxVideoBitrate,\n        selectedTrackIndices);\n\n    return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);\n  }\n\n  private static int getAdaptiveVideoTrackCountForMimeType(\n      TrackGroup group,\n      @Capabilities int[] formatSupport,\n      int requiredAdaptiveSupport,\n      @Nullable String mimeType,\n      int maxVideoWidth,\n      int maxVideoHeight,\n      int maxVideoFrameRate,\n      int maxVideoBitrate,\n      List<Integer> selectedTrackIndices) {\n    int adaptiveTrackCount = 0;\n    for (int i = 0; i < selectedTrackIndices.size(); i++) {\n      int trackIndex = selectedTrackIndices.get(i);\n      if (isSupportedAdaptiveVideoTrack(\n          group.getFormat(trackIndex),\n          mimeType,\n          formatSupport[trackIndex],\n          requiredAdaptiveSupport,\n          maxVideoWidth,\n          maxVideoHeight,\n          maxVideoFrameRate,\n          maxVideoBitrate)) {\n        adaptiveTrackCount++;\n      }\n    }\n    return adaptiveTrackCount;\n  }\n\n  private static void filterAdaptiveVideoTrackCountForMimeType(\n      TrackGroup group,\n      @Capabilities int[] formatSupport,\n      int requiredAdaptiveSupport,\n      @Nullable String mimeType,\n      int maxVideoWidth,\n      int maxVideoHeight,\n      int maxVideoFrameRate,\n      int maxVideoBitrate,\n      List<Integer> selectedTrackIndices) {\n    for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {\n      int trackIndex = selectedTrackIndices.get(i);\n      if (!isSupportedAdaptiveVideoTrack(\n          group.getFormat(trackIndex),\n          mimeType,\n          formatSupport[trackIndex],\n          requiredAdaptiveSupport,\n          maxVideoWidth,\n          maxVideoHeight,\n          maxVideoFrameRate,\n          maxVideoBitrate)) {\n        selectedTrackIndices.remove(i);\n      }\n    }\n  }\n\n  private static boolean isSupportedAdaptiveVideoTrack(\n      Format format,\n      @Nullable String mimeType,\n      @Capabilities int formatSupport,\n      int requiredAdaptiveSupport,\n      int maxVideoWidth,\n      int maxVideoHeight,\n      int maxVideoFrameRate,\n      int maxVideoBitrate) {\n    return isSupported(formatSupport, false)\n        && ((formatSupport & requiredAdaptiveSupport) != 0)\n        && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))\n        && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)\n        && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight)\n        && (format.frameRate == Format.NO_VALUE || format.frameRate <= maxVideoFrameRate)\n        && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);\n  }\n\n  @Nullable\n  private static TrackSelection.Definition selectFixedVideoTrack(\n      TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) {\n    TrackGroup selectedGroup = null;\n    int selectedTrackIndex = 0;\n    int selectedTrackScore = 0;\n    int selectedBitrate = Format.NO_VALUE;\n    int selectedPixelCount = Format.NO_VALUE;\n    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {\n      TrackGroup trackGroup = groups.get(groupIndex);\n      List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup,\n          params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);\n      @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];\n      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n        if (isSupported(trackFormatSupport[trackIndex],\n            params.exceedRendererCapabilitiesIfNecessary)) {\n          Format format = trackGroup.getFormat(trackIndex);\n          boolean isWithinConstraints =\n              selectedTrackIndices.contains(trackIndex)\n                  && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth)\n                  && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight)\n                  && (format.frameRate == Format.NO_VALUE\n                      || format.frameRate <= params.maxVideoFrameRate)\n                  && (format.bitrate == Format.NO_VALUE\n                      || format.bitrate <= params.maxVideoBitrate);\n          if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) {\n            // Track should not be selected.\n            continue;\n          }\n          int trackScore = isWithinConstraints ? 2 : 1;\n          boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false);\n          if (isWithinCapabilities) {\n            trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;\n          }\n          boolean selectTrack = trackScore > selectedTrackScore;\n          if (trackScore == selectedTrackScore) {\n            int bitrateComparison = compareFormatValues(format.bitrate, selectedBitrate);\n            if (params.forceLowestBitrate && bitrateComparison != 0) {\n              // Use bitrate as a tie breaker, preferring the lower bitrate.\n              selectTrack = bitrateComparison < 0;\n            } else {\n              // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If\n              // we're within constraints prefer a higher pixel count (or bitrate), else prefer a\n              // lower count (or bitrate). If still tied then prefer the first track (i.e. the one\n              // that's already selected).\n              int formatPixelCount = format.getPixelCount();\n              int comparisonResult = formatPixelCount != selectedPixelCount\n                  ? compareFormatValues(formatPixelCount, selectedPixelCount)\n                  : compareFormatValues(format.bitrate, selectedBitrate);\n              selectTrack = isWithinCapabilities && isWithinConstraints\n                  ? comparisonResult > 0 : comparisonResult < 0;\n            }\n          }\n          if (selectTrack) {\n            selectedGroup = trackGroup;\n            selectedTrackIndex = trackIndex;\n            selectedTrackScore = trackScore;\n            selectedBitrate = format.bitrate;\n            selectedPixelCount = format.getPixelCount();\n          }\n        }\n      }\n    }\n    return selectedGroup == null\n        ? null\n        : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);\n  }\n\n  // Audio track selection implementation.\n\n  /**\n   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a\n   * {@link TrackSelection} for an audio renderer.\n   *\n   * @param groups The {@link TrackGroupArray} mapped to the renderer.\n   * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer,\n   *     track group and track (in that order).\n   * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type\n   *     adaptation for the renderer.\n   * @param params The selector's current constraint parameters.\n   * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.\n   * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or\n   *     null if no selection was made.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  @SuppressWarnings(\"unused\")\n  @Nullable\n  protected Pair<TrackSelection.Definition, AudioTrackScore> selectAudioTrack(\n      TrackGroupArray groups,\n      @Capabilities int[][] formatSupports,\n      @AdaptiveSupport int mixedMimeTypeAdaptationSupports,\n      Parameters params,\n      boolean enableAdaptiveTrackSelection)\n      throws ExoPlaybackException {\n    int selectedTrackIndex = C.INDEX_UNSET;\n    int selectedGroupIndex = C.INDEX_UNSET;\n    AudioTrackScore selectedTrackScore = null;\n    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {\n      TrackGroup trackGroup = groups.get(groupIndex);\n      @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];\n      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n        if (isSupported(trackFormatSupport[trackIndex],\n            params.exceedRendererCapabilitiesIfNecessary)) {\n          Format format = trackGroup.getFormat(trackIndex);\n          AudioTrackScore trackScore =\n              new AudioTrackScore(format, params, trackFormatSupport[trackIndex]);\n          if (!trackScore.isWithinConstraints && !params.exceedAudioConstraintsIfNecessary) {\n            // Track should not be selected.\n            continue;\n          }\n          if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) {\n            selectedGroupIndex = groupIndex;\n            selectedTrackIndex = trackIndex;\n            selectedTrackScore = trackScore;\n          }\n        }\n      }\n    }\n\n    if (selectedGroupIndex == C.INDEX_UNSET) {\n      return null;\n    }\n\n    TrackGroup selectedGroup = groups.get(selectedGroupIndex);\n\n    TrackSelection.Definition definition = null;\n    if (!params.forceHighestSupportedBitrate\n        && !params.forceLowestBitrate\n        && enableAdaptiveTrackSelection) {\n      // If the group of the track with the highest score allows it, try to enable adaptation.\n      int[] adaptiveTracks =\n          getAdaptiveAudioTracks(\n              selectedGroup,\n              formatSupports[selectedGroupIndex],\n              params.maxAudioBitrate,\n              params.allowAudioMixedMimeTypeAdaptiveness,\n              params.allowAudioMixedSampleRateAdaptiveness,\n              params.allowAudioMixedChannelCountAdaptiveness);\n      if (adaptiveTracks.length > 0) {\n        definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks);\n      }\n    }\n    if (definition == null) {\n      // We didn't make an adaptive selection, so make a fixed one instead.\n      definition = new TrackSelection.Definition(selectedGroup, selectedTrackIndex);\n    }\n\n    return Pair.create(definition, Assertions.checkNotNull(selectedTrackScore));\n  }\n\n  private static int[] getAdaptiveAudioTracks(\n      TrackGroup group,\n      @Capabilities int[] formatSupport,\n      int maxAudioBitrate,\n      boolean allowMixedMimeTypeAdaptiveness,\n      boolean allowMixedSampleRateAdaptiveness,\n      boolean allowAudioMixedChannelCountAdaptiveness) {\n    int selectedConfigurationTrackCount = 0;\n    AudioConfigurationTuple selectedConfiguration = null;\n    HashSet<AudioConfigurationTuple> seenConfigurationTuples = new HashSet<>();\n    for (int i = 0; i < group.length; i++) {\n      Format format = group.getFormat(i);\n      AudioConfigurationTuple configuration =\n          new AudioConfigurationTuple(\n              format.channelCount, format.sampleRate, format.sampleMimeType);\n      if (seenConfigurationTuples.add(configuration)) {\n        int configurationCount =\n            getAdaptiveAudioTrackCount(\n                group,\n                formatSupport,\n                configuration,\n                maxAudioBitrate,\n                allowMixedMimeTypeAdaptiveness,\n                allowMixedSampleRateAdaptiveness,\n                allowAudioMixedChannelCountAdaptiveness);\n        if (configurationCount > selectedConfigurationTrackCount) {\n          selectedConfiguration = configuration;\n          selectedConfigurationTrackCount = configurationCount;\n        }\n      }\n    }\n\n    if (selectedConfigurationTrackCount > 1) {\n      Assertions.checkNotNull(selectedConfiguration);\n      int[] adaptiveIndices = new int[selectedConfigurationTrackCount];\n      int index = 0;\n      for (int i = 0; i < group.length; i++) {\n        Format format = group.getFormat(i);\n        if (isSupportedAdaptiveAudioTrack(\n            format,\n            formatSupport[i],\n            selectedConfiguration,\n            maxAudioBitrate,\n            allowMixedMimeTypeAdaptiveness,\n            allowMixedSampleRateAdaptiveness,\n            allowAudioMixedChannelCountAdaptiveness)) {\n          adaptiveIndices[index++] = i;\n        }\n      }\n      return adaptiveIndices;\n    }\n    return NO_TRACKS;\n  }\n\n  private static int getAdaptiveAudioTrackCount(\n      TrackGroup group,\n      @Capabilities int[] formatSupport,\n      AudioConfigurationTuple configuration,\n      int maxAudioBitrate,\n      boolean allowMixedMimeTypeAdaptiveness,\n      boolean allowMixedSampleRateAdaptiveness,\n      boolean allowAudioMixedChannelCountAdaptiveness) {\n    int count = 0;\n    for (int i = 0; i < group.length; i++) {\n      if (isSupportedAdaptiveAudioTrack(\n          group.getFormat(i),\n          formatSupport[i],\n          configuration,\n          maxAudioBitrate,\n          allowMixedMimeTypeAdaptiveness,\n          allowMixedSampleRateAdaptiveness,\n          allowAudioMixedChannelCountAdaptiveness)) {\n        count++;\n      }\n    }\n    return count;\n  }\n\n  private static boolean isSupportedAdaptiveAudioTrack(\n      Format format,\n      @Capabilities int formatSupport,\n      AudioConfigurationTuple configuration,\n      int maxAudioBitrate,\n      boolean allowMixedMimeTypeAdaptiveness,\n      boolean allowMixedSampleRateAdaptiveness,\n      boolean allowAudioMixedChannelCountAdaptiveness) {\n    return isSupported(formatSupport, false)\n        && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate)\n        && (allowAudioMixedChannelCountAdaptiveness\n            || (format.channelCount != Format.NO_VALUE\n                && format.channelCount == configuration.channelCount))\n        && (allowMixedMimeTypeAdaptiveness\n            || (format.sampleMimeType != null\n                && TextUtils.equals(format.sampleMimeType, configuration.mimeType)))\n        && (allowMixedSampleRateAdaptiveness\n            || (format.sampleRate != Format.NO_VALUE\n                && format.sampleRate == configuration.sampleRate));\n  }\n\n  // Text track selection implementation.\n\n  /**\n   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a\n   * {@link TrackSelection} for a text renderer.\n   *\n   * @param groups The {@link TrackGroupArray} mapped to the renderer.\n   * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track\n   *     group and track (in that order).\n   * @param params The selector's current constraint parameters.\n   * @param selectedAudioLanguage The language of the selected audio track. May be null if the\n   *     selected text track declares no language or no text track was selected.\n   * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null\n   *     if no selection was made.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  @Nullable\n  protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(\n      TrackGroupArray groups,\n      @Capabilities int[][] formatSupport,\n      Parameters params,\n      @Nullable String selectedAudioLanguage)\n      throws ExoPlaybackException {\n    TrackGroup selectedGroup = null;\n    int selectedTrackIndex = C.INDEX_UNSET;\n    TextTrackScore selectedTrackScore = null;\n    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {\n      TrackGroup trackGroup = groups.get(groupIndex);\n      @Capabilities int[] trackFormatSupport = formatSupport[groupIndex];\n      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n        if (isSupported(trackFormatSupport[trackIndex],\n            params.exceedRendererCapabilitiesIfNecessary)) {\n          Format format = trackGroup.getFormat(trackIndex);\n          TextTrackScore trackScore =\n              new TextTrackScore(\n                  format, params, trackFormatSupport[trackIndex], selectedAudioLanguage);\n          if (trackScore.isWithinConstraints\n              && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) {\n            selectedGroup = trackGroup;\n            selectedTrackIndex = trackIndex;\n            selectedTrackScore = trackScore;\n          }\n        }\n      }\n    }\n    return selectedGroup == null\n        ? null\n        : Pair.create(\n            new TrackSelection.Definition(selectedGroup, selectedTrackIndex),\n            Assertions.checkNotNull(selectedTrackScore));\n  }\n\n  // General track selection methods.\n\n  /**\n   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a\n   * {@link TrackSelection} for a renderer whose type is neither video, audio or text.\n   *\n   * @param trackType The type of the renderer.\n   * @param groups The {@link TrackGroupArray} mapped to the renderer.\n   * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track\n   *     group and track (in that order).\n   * @param params The selector's current constraint parameters.\n   * @return The {@link TrackSelection} for the renderer, or null if no selection was made.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  @Nullable\n  protected TrackSelection.Definition selectOtherTrack(\n      int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params)\n      throws ExoPlaybackException {\n    TrackGroup selectedGroup = null;\n    int selectedTrackIndex = 0;\n    int selectedTrackScore = 0;\n    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {\n      TrackGroup trackGroup = groups.get(groupIndex);\n      @Capabilities int[] trackFormatSupport = formatSupport[groupIndex];\n      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n        if (isSupported(trackFormatSupport[trackIndex],\n            params.exceedRendererCapabilitiesIfNecessary)) {\n          Format format = trackGroup.getFormat(trackIndex);\n          boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;\n          int trackScore = isDefault ? 2 : 1;\n          if (isSupported(trackFormatSupport[trackIndex], false)) {\n            trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;\n          }\n          if (trackScore > selectedTrackScore) {\n            selectedGroup = trackGroup;\n            selectedTrackIndex = trackIndex;\n            selectedTrackScore = trackScore;\n          }\n        }\n      }\n    }\n    return selectedGroup == null\n        ? null\n        : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);\n  }\n\n  // Utility methods.\n\n  /**\n   * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in\n   * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate\n   * renderers if so.\n   *\n   * @param mappedTrackInfo Mapped track information.\n   * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by\n   *     renderer, track group and track (in that order).\n   * @param rendererConfigurations The renderer configurations. Configurations may be replaced with\n   *     ones that enable tunneling as a result of this call.\n   * @param trackSelections The renderer track selections.\n   * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link\n   *     C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.\n   */\n  private static void maybeConfigureRenderersForTunneling(\n      MappedTrackInfo mappedTrackInfo,\n      @Capabilities int[][][] renderererFormatSupports,\n      @NullableType RendererConfiguration[] rendererConfigurations,\n      @NullableType TrackSelection[] trackSelections,\n      int tunnelingAudioSessionId) {\n    if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) {\n      return;\n    }\n    // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and\n    // one video renderer to support tunneling and have a selection.\n    int tunnelingAudioRendererIndex = -1;\n    int tunnelingVideoRendererIndex = -1;\n    boolean enableTunneling = true;\n    for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {\n      int rendererType = mappedTrackInfo.getRendererType(i);\n      TrackSelection trackSelection = trackSelections[i];\n      if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO)\n          && trackSelection != null) {\n        if (rendererSupportsTunneling(\n            renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) {\n          if (rendererType == C.TRACK_TYPE_AUDIO) {\n            if (tunnelingAudioRendererIndex != -1) {\n              enableTunneling = false;\n              break;\n            } else {\n              tunnelingAudioRendererIndex = i;\n            }\n          } else {\n            if (tunnelingVideoRendererIndex != -1) {\n              enableTunneling = false;\n              break;\n            } else {\n              tunnelingVideoRendererIndex = i;\n            }\n          }\n        }\n      }\n    }\n    enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1;\n    if (enableTunneling) {\n      RendererConfiguration tunnelingRendererConfiguration =\n          new RendererConfiguration(tunnelingAudioSessionId);\n      rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration;\n      rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration;\n    }\n  }\n\n  /**\n   * Returns whether a renderer supports tunneling for a {@link TrackSelection}.\n   *\n   * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track\n   *     index (in that order).\n   * @param trackGroups The {@link TrackGroupArray}s for the renderer.\n   * @param selection The track selection.\n   * @return Whether the renderer supports tunneling for the {@link TrackSelection}.\n   */\n  private static boolean rendererSupportsTunneling(\n      @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) {\n    if (selection == null) {\n      return false;\n    }\n    int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());\n    for (int i = 0; i < selection.length(); i++) {\n      @Capabilities\n      int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)];\n      if (RendererCapabilities.getTunnelingSupport(trackFormatSupport)\n          != RendererCapabilities.TUNNELING_SUPPORTED) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Compares two format values for order. A known value is considered greater than {@link\n   * Format#NO_VALUE}.\n   *\n   * @param first The first value.\n   * @param second The second value.\n   * @return A negative integer if the first value is less than the second. Zero if they are equal.\n   *     A positive integer if the first value is greater than the second.\n   */\n  private static int compareFormatValues(int first, int second) {\n    return first == Format.NO_VALUE\n        ? (second == Format.NO_VALUE ? 0 : -1)\n        : (second == Format.NO_VALUE ? 1 : (first - second));\n  }\n\n  /**\n   * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link\n   * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the\n   * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   *\n   * @param formatSupport {@link Capabilities}.\n   * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link\n   *     RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if\n   *     {@code allowExceedsCapabilities} is set and the format support is {@link\n   *     RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   */\n  protected static boolean isSupported(\n      @Capabilities int formatSupport, boolean allowExceedsCapabilities) {\n    @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport);\n    return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities\n        && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES);\n  }\n\n  /**\n   * Normalizes the input string to null if it does not define a language, or returns it otherwise.\n   *\n   * @param language The string.\n   * @return The string, optionally normalized to null if it does not define a language.\n   */\n  @Nullable\n  protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) {\n    return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED)\n        ? null\n        : language;\n  }\n\n  /**\n   * Returns a score for how well a language specified in a {@link Format} matches a given language.\n   *\n   * @param format The {@link Format}.\n   * @param language The language, or null.\n   * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format\n   *     language tag are allowed.\n   * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly,\n   *     a score of 2 if the languages don't match but belong to the same main language, a score of\n   *     1 if the format language is undetermined and such a match is allowed, and a score of 0 if\n   *     the languages don't match at all.\n   */\n  protected static int getFormatLanguageScore(\n      Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) {\n    if (!TextUtils.isEmpty(language) && language.equals(format.language)) {\n      // Full literal match of non-empty languages, including matches of an explicit \"und\" query.\n      return 4;\n    }\n    language = normalizeUndeterminedLanguageToNull(language);\n    String formatLanguage = normalizeUndeterminedLanguageToNull(format.language);\n    if (formatLanguage == null || language == null) {\n      // At least one of the languages is undetermined.\n      return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0;\n    }\n    if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) {\n      // Partial match where one language is a subset of the other (e.g. \"zh-hans\" and \"zh-hans-hk\")\n      return 3;\n    }\n    String formatMainLanguage = Util.splitAtFirst(formatLanguage, \"-\")[0];\n    String queryMainLanguage = Util.splitAtFirst(language, \"-\")[0];\n    if (formatMainLanguage.equals(queryMainLanguage)) {\n      // Partial match where only the main language tag is the same (e.g. \"fr-fr\" and \"fr-ca\")\n      return 2;\n    }\n    return 0;\n  }\n\n  private static List<Integer> getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,\n      int viewportHeight, boolean orientationMayChange) {\n    // Initially include all indices.\n    ArrayList<Integer> selectedTrackIndices = new ArrayList<>(group.length);\n    for (int i = 0; i < group.length; i++) {\n      selectedTrackIndices.add(i);\n    }\n\n    if (viewportWidth == Integer.MAX_VALUE || viewportHeight == Integer.MAX_VALUE) {\n      // Viewport dimensions not set. Return the full set of indices.\n      return selectedTrackIndices;\n    }\n\n    int maxVideoPixelsToRetain = Integer.MAX_VALUE;\n    for (int i = 0; i < group.length; i++) {\n      Format format = group.getFormat(i);\n      // Keep track of the number of pixels of the selected format whose resolution is the\n      // smallest to exceed the maximum size at which it can be displayed within the viewport.\n      // We'll discard formats of higher resolution.\n      if (format.width > 0 && format.height > 0) {\n        Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange,\n            viewportWidth, viewportHeight, format.width, format.height);\n        int videoPixels = format.width * format.height;\n        if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)\n            && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)\n            && videoPixels < maxVideoPixelsToRetain) {\n          maxVideoPixelsToRetain = videoPixels;\n        }\n      }\n    }\n\n    // Filter out formats that exceed maxVideoPixelsToRetain. These formats have an unnecessarily\n    // high resolution given the size at which the video will be displayed within the viewport. Also\n    // filter out formats with unknown dimensions, since we have some whose dimensions are known.\n    if (maxVideoPixelsToRetain != Integer.MAX_VALUE) {\n      for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {\n        Format format = group.getFormat(selectedTrackIndices.get(i));\n        int pixelCount = format.getPixelCount();\n        if (pixelCount == Format.NO_VALUE || pixelCount > maxVideoPixelsToRetain) {\n          selectedTrackIndices.remove(i);\n        }\n      }\n    }\n\n    return selectedTrackIndices;\n  }\n\n  /**\n   * Given viewport dimensions and video dimensions, computes the maximum size of the video as it\n   * will be rendered to fit inside of the viewport.\n   */\n  private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth,\n      int viewportHeight, int videoWidth, int videoHeight) {\n    if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {\n      // Rotation is allowed, and the video will be larger in the rotated viewport.\n      int tempViewportWidth = viewportWidth;\n      viewportWidth = viewportHeight;\n      viewportHeight = tempViewportWidth;\n    }\n\n    if (videoWidth * viewportHeight >= videoHeight * viewportWidth) {\n      // Horizontal letter-boxing along top and bottom.\n      return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth));\n    } else {\n      // Vertical letter-boxing along edges.\n      return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight);\n    }\n  }\n\n  /**\n   * Compares two integers in a safe way avoiding potential overflow.\n   *\n   * @param first The first value.\n   * @param second The second value.\n   * @return A negative integer if the first value is less than the second. Zero if they are equal.\n   *     A positive integer if the first value is greater than the second.\n   */\n  private static int compareInts(int first, int second) {\n    return first > second ? 1 : (second > first ? -1 : 0);\n  }\n\n  /** Represents how well an audio track matches the selection {@link Parameters}. */\n  protected static final class AudioTrackScore implements Comparable<AudioTrackScore> {\n\n    /**\n     * Whether the provided format is within the parameter constraints. If {@code false}, the format\n     * should not be selected.\n     */\n    public final boolean isWithinConstraints;\n\n    @Nullable private final String language;\n    private final Parameters parameters;\n    private final boolean isWithinRendererCapabilities;\n    private final int preferredLanguageScore;\n    private final int localeLanguageMatchIndex;\n    private final int localeLanguageScore;\n    private final boolean isDefaultSelectionFlag;\n    private final int channelCount;\n    private final int sampleRate;\n    private final int bitrate;\n\n    public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) {\n      this.parameters = parameters;\n      this.language = normalizeUndeterminedLanguageToNull(format.language);\n      isWithinRendererCapabilities = isSupported(formatSupport, false);\n      preferredLanguageScore =\n          getFormatLanguageScore(\n              format,\n              parameters.preferredAudioLanguage,\n              /* allowUndeterminedFormatLanguage= */ false);\n      isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;\n      channelCount = format.channelCount;\n      sampleRate = format.sampleRate;\n      bitrate = format.bitrate;\n      isWithinConstraints =\n          (format.bitrate == Format.NO_VALUE || format.bitrate <= parameters.maxAudioBitrate)\n              && (format.channelCount == Format.NO_VALUE\n                  || format.channelCount <= parameters.maxAudioChannelCount);\n      String[] localeLanguages = Util.getSystemLanguageCodes();\n      int bestMatchIndex = Integer.MAX_VALUE;\n      int bestMatchScore = 0;\n      for (int i = 0; i < localeLanguages.length; i++) {\n        int score =\n            getFormatLanguageScore(\n                format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false);\n        if (score > 0) {\n          bestMatchIndex = i;\n          bestMatchScore = score;\n          break;\n        }\n      }\n      localeLanguageMatchIndex = bestMatchIndex;\n      localeLanguageScore = bestMatchScore;\n    }\n\n    /**\n     * Compares this score with another.\n     *\n     * @param other The other score to compare to.\n     * @return A positive integer if this score is better than the other. Zero if they are equal. A\n     *     negative integer if this score is worse than the other.\n     */\n    @Override\n    public int compareTo(AudioTrackScore other) {\n      if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {\n        return this.isWithinRendererCapabilities ? 1 : -1;\n      }\n      if (this.preferredLanguageScore != other.preferredLanguageScore) {\n        return compareInts(this.preferredLanguageScore, other.preferredLanguageScore);\n      }\n      if (this.isWithinConstraints != other.isWithinConstraints) {\n        return this.isWithinConstraints ? 1 : -1;\n      }\n      if (parameters.forceLowestBitrate) {\n        int bitrateComparison = compareFormatValues(bitrate, other.bitrate);\n        if (bitrateComparison != 0) {\n          return bitrateComparison > 0 ? -1 : 1;\n        }\n      }\n      if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) {\n        return this.isDefaultSelectionFlag ? 1 : -1;\n      }\n      if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) {\n        return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex);\n      }\n      if (this.localeLanguageScore != other.localeLanguageScore) {\n        return compareInts(this.localeLanguageScore, other.localeLanguageScore);\n      }\n      // If the formats are within constraints and renderer capabilities then prefer higher values\n      // of channel count, sample rate and bit rate in that order. Otherwise, prefer lower values.\n      int resultSign = isWithinConstraints && isWithinRendererCapabilities ? 1 : -1;\n      if (this.channelCount != other.channelCount) {\n        return resultSign * compareInts(this.channelCount, other.channelCount);\n      }\n      if (this.sampleRate != other.sampleRate) {\n        return resultSign * compareInts(this.sampleRate, other.sampleRate);\n      }\n      if (Util.areEqual(this.language, other.language)) {\n        // Only compare bit rates of tracks with the same or unknown language.\n        return resultSign * compareInts(this.bitrate, other.bitrate);\n      }\n      return 0;\n    }\n  }\n\n  private static final class AudioConfigurationTuple {\n\n    public final int channelCount;\n    public final int sampleRate;\n    @Nullable public final String mimeType;\n\n    public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) {\n      this.channelCount = channelCount;\n      this.sampleRate = sampleRate;\n      this.mimeType = mimeType;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n      if (this == obj) {\n        return true;\n      }\n      if (obj == null || getClass() != obj.getClass()) {\n        return false;\n      }\n      AudioConfigurationTuple other = (AudioConfigurationTuple) obj;\n      return channelCount == other.channelCount && sampleRate == other.sampleRate\n          && TextUtils.equals(mimeType, other.mimeType);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = channelCount;\n      result = 31 * result + sampleRate;\n      result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);\n      return result;\n    }\n\n  }\n\n  /** Represents how well a text track matches the selection {@link Parameters}. */\n  protected static final class TextTrackScore implements Comparable<TextTrackScore> {\n\n    /**\n     * Whether the provided format is within the parameter constraints. If {@code false}, the format\n     * should not be selected.\n     */\n    public final boolean isWithinConstraints;\n\n    private final boolean isWithinRendererCapabilities;\n    private final boolean isDefault;\n    private final boolean hasPreferredIsForcedFlag;\n    private final int preferredLanguageScore;\n    private final int preferredRoleFlagsScore;\n    private final int selectedAudioLanguageScore;\n    private final boolean hasCaptionRoleFlags;\n\n    public TextTrackScore(\n        Format format,\n        Parameters parameters,\n        @Capabilities int trackFormatSupport,\n        @Nullable String selectedAudioLanguage) {\n      isWithinRendererCapabilities =\n          isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);\n      int maskedSelectionFlags =\n          format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags;\n      isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;\n      boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;\n      preferredLanguageScore =\n          getFormatLanguageScore(\n              format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage);\n      preferredRoleFlagsScore =\n          Integer.bitCount(format.roleFlags & parameters.preferredTextRoleFlags);\n      hasCaptionRoleFlags =\n          (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0;\n      // Prefer non-forced to forced if a preferred text language has been matched. Where both are\n      // provided the non-forced track will usually contain the forced subtitles as a subset.\n      // Otherwise, prefer a forced track.\n      hasPreferredIsForcedFlag =\n          (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced);\n      boolean selectedAudioLanguageUndetermined =\n          normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null;\n      selectedAudioLanguageScore =\n          getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined);\n      isWithinConstraints =\n          preferredLanguageScore > 0\n              || (parameters.preferredTextLanguage == null && preferredRoleFlagsScore > 0)\n              || isDefault\n              || (isForced && selectedAudioLanguageScore > 0);\n    }\n\n    /**\n     * Compares this score with another.\n     *\n     * @param other The other score to compare to.\n     * @return A positive integer if this score is better than the other. Zero if they are equal. A\n     *     negative integer if this score is worse than the other.\n     */\n    @Override\n    public int compareTo(TextTrackScore other) {\n      if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {\n        return this.isWithinRendererCapabilities ? 1 : -1;\n      }\n      if (this.preferredLanguageScore != other.preferredLanguageScore) {\n        return compareInts(this.preferredLanguageScore, other.preferredLanguageScore);\n      }\n      if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) {\n        return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore);\n      }\n      if (this.isDefault != other.isDefault) {\n        return this.isDefault ? 1 : -1;\n      }\n      if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) {\n        return this.hasPreferredIsForcedFlag ? 1 : -1;\n      }\n      if (this.selectedAudioLanguageScore != other.selectedAudioLanguageScore) {\n        return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore);\n      }\n      if (preferredRoleFlagsScore == 0 && this.hasCaptionRoleFlags != other.hasCaptionRoleFlags) {\n        return this.hasCaptionRoleFlags ? -1 : 1;\n      }\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A {@link TrackSelection} consisting of a single track.\n */\npublic final class FixedTrackSelection extends BaseTrackSelection {\n\n  /**\n   * @deprecated Don't use as adaptive track selection factory as it will throw when multiple tracks\n   *     are selected. If you would like to disable adaptive selection in {@link\n   *     DefaultTrackSelector}, enable the {@link\n   *     DefaultTrackSelector.Parameters#forceHighestSupportedBitrate} flag instead.\n   */\n  @Deprecated\n  public static final class Factory implements TrackSelection.Factory {\n\n    private final int reason;\n    @Nullable private final Object data;\n\n    public Factory() {\n      this.reason = C.SELECTION_REASON_UNKNOWN;\n      this.data = null;\n    }\n\n    /**\n     * @param reason A reason for the track selection.\n     * @param data Optional data associated with the track selection.\n     */\n    public Factory(int reason, @Nullable Object data) {\n      this.reason = reason;\n      this.data = data;\n    }\n\n    @Override\n    public @NullableType TrackSelection[] createTrackSelections(\n        @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n      return TrackSelectionUtil.createTrackSelectionsForDefinitions(\n          definitions,\n          definition ->\n              new FixedTrackSelection(definition.group, definition.tracks[0], reason, data));\n    }\n  }\n\n  private final int reason;\n  @Nullable private final Object data;\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param track The index of the selected track within the {@link TrackGroup}.\n   */\n  public FixedTrackSelection(TrackGroup group, int track) {\n    this(group, track, C.SELECTION_REASON_UNKNOWN, null);\n  }\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param track The index of the selected track within the {@link TrackGroup}.\n   * @param reason A reason for the track selection.\n   * @param data Optional data associated with the track selection.\n   */\n  public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) {\n    super(group, track);\n    this.reason = reason;\n    this.data = data;\n  }\n\n  @Override\n  public void updateSelectedTrack(\n      long playbackPositionUs,\n      long bufferedDurationUs,\n      long availableDurationUs,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] mediaChunkIterators) {\n    // Do nothing.\n  }\n\n  @Override\n  public int getSelectedIndex() {\n    return 0;\n  }\n\n  @Override\n  public int getSelectionReason() {\n    return reason;\n  }\n\n  @Override\n  @Nullable\n  public Object getSelectionData() {\n    return data;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.util.Pair;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;\nimport com.google.android.exoplayer2.RendererCapabilities.Capabilities;\nimport com.google.android.exoplayer2.RendererCapabilities.FormatSupport;\nimport com.google.android.exoplayer2.RendererConfiguration;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s\n * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each\n * renderer.\n */\npublic abstract class MappingTrackSelector extends TrackSelector {\n\n  /**\n   * Provides mapped track information for each renderer.\n   */\n  public static final class MappedTrackInfo {\n\n    /**\n     * Levels of renderer support. Higher numerical values indicate higher levels of support. One of\n     * {@link #RENDERER_SUPPORT_NO_TRACKS}, {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS}, {@link\n     * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS} or {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}.\n     */\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({\n      RENDERER_SUPPORT_NO_TRACKS,\n      RENDERER_SUPPORT_UNSUPPORTED_TRACKS,\n      RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS,\n      RENDERER_SUPPORT_PLAYABLE_TRACKS\n    })\n    @interface RendererSupport {}\n    /** The renderer does not have any associated tracks. */\n    public static final int RENDERER_SUPPORT_NO_TRACKS = 0;\n    /**\n     * The renderer has tracks mapped to it, but all are unsupported. In other words, {@link\n     * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},\n     * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} or {@link\n     * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all tracks mapped to the renderer.\n     */\n    public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;\n    /**\n     * The renderer has tracks mapped to it and at least one is of a supported type, but all such\n     * tracks exceed the renderer's capabilities. In other words, {@link #getTrackSupport(int, int,\n     * int)} returns {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} for at least one\n     * track mapped to the renderer, but does not return {@link\n     * RendererCapabilities#FORMAT_HANDLED} for any tracks mapped to the renderer.\n     */\n    public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;\n    /**\n     * The renderer has tracks mapped to it, and at least one such track is playable. In other\n     * words, {@link #getTrackSupport(int, int, int)} returns {@link\n     * RendererCapabilities#FORMAT_HANDLED} for at least one track mapped to the renderer.\n     */\n    public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;\n\n    /** @deprecated Use {@link #getRendererCount()}. */\n    @Deprecated public final int length;\n\n    private final int rendererCount;\n    private final int[] rendererTrackTypes;\n    private final TrackGroupArray[] rendererTrackGroups;\n    @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports;\n    @Capabilities private final int[][][] rendererFormatSupports;\n    private final TrackGroupArray unmappedTrackGroups;\n\n    /**\n     * @param rendererTrackTypes The track type handled by each renderer.\n     * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer.\n     * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type\n     *     adaptation for the renderer.\n     * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by\n     *     renderer, track group and track (in that order).\n     * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer.\n     */\n    @SuppressWarnings(\"deprecation\")\n    /* package */ MappedTrackInfo(\n        int[] rendererTrackTypes,\n        TrackGroupArray[] rendererTrackGroups,\n        @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports,\n        @Capabilities int[][][] rendererFormatSupports,\n        TrackGroupArray unmappedTrackGroups) {\n      this.rendererTrackTypes = rendererTrackTypes;\n      this.rendererTrackGroups = rendererTrackGroups;\n      this.rendererFormatSupports = rendererFormatSupports;\n      this.rendererMixedMimeTypeAdaptiveSupports = rendererMixedMimeTypeAdaptiveSupports;\n      this.unmappedTrackGroups = unmappedTrackGroups;\n      this.rendererCount = rendererTrackTypes.length;\n      this.length = rendererCount;\n    }\n\n    /** Returns the number of renderers. */\n    public int getRendererCount() {\n      return rendererCount;\n    }\n\n    /**\n     * Returns the track type that the renderer at a given index handles.\n     *\n     * @see Renderer#getTrackType()\n     * @param rendererIndex The renderer index.\n     * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.\n     */\n    public int getRendererType(int rendererIndex) {\n      return rendererTrackTypes[rendererIndex];\n    }\n\n    /**\n     * Returns the {@link TrackGroup}s mapped to the renderer at the specified index.\n     *\n     * @param rendererIndex The renderer index.\n     * @return The corresponding {@link TrackGroup}s.\n     */\n    public TrackGroupArray getTrackGroups(int rendererIndex) {\n      return rendererTrackGroups[rendererIndex];\n    }\n\n    /**\n     * Returns the extent to which a renderer can play the tracks that are mapped to it.\n     *\n     * @param rendererIndex The renderer index.\n     * @return The {@link RendererSupport}.\n     */\n    @RendererSupport\n    public int getRendererSupport(int rendererIndex) {\n      @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;\n      @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex];\n      for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) {\n        for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) {\n          int trackRendererSupport;\n          switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) {\n            case RendererCapabilities.FORMAT_HANDLED:\n              return RENDERER_SUPPORT_PLAYABLE_TRACKS;\n            case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:\n              trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;\n              break;\n            case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:\n            case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:\n            case RendererCapabilities.FORMAT_UNSUPPORTED_DRM:\n              trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;\n              break;\n            default:\n              throw new IllegalStateException();\n          }\n          bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);\n        }\n      }\n      return bestRendererSupport;\n    }\n\n    /** @deprecated Use {@link #getTypeSupport(int)}. */\n    @Deprecated\n    @RendererSupport\n    public int getTrackTypeRendererSupport(int trackType) {\n      return getTypeSupport(trackType);\n    }\n\n    /**\n     * Returns the extent to which tracks of a specified type are supported. This is the best level\n     * of support obtained from {@link #getRendererSupport(int)} for all renderers that handle the\n     * specified type. If no such renderers exist then {@link #RENDERER_SUPPORT_NO_TRACKS} is\n     * returned.\n     *\n     * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.\n     * @return The {@link RendererSupport}.\n     */\n    @RendererSupport\n    public int getTypeSupport(int trackType) {\n      @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;\n      for (int i = 0; i < rendererCount; i++) {\n        if (rendererTrackTypes[i] == trackType) {\n          bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));\n        }\n      }\n      return bestRendererSupport;\n    }\n\n    /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */\n    @Deprecated\n    @FormatSupport\n    public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {\n      return getTrackSupport(rendererIndex, groupIndex, trackIndex);\n    }\n\n    /**\n     * Returns the extent to which an individual track is supported by the renderer.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groupIndex The index of the track group to which the track belongs.\n     * @param trackIndex The index of the track within the track group.\n     * @return The {@link FormatSupport}.\n     */\n    @FormatSupport\n    public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) {\n      return RendererCapabilities.getFormatSupport(\n          rendererFormatSupports[rendererIndex][groupIndex][trackIndex]);\n    }\n\n    /**\n     * Returns the extent to which a renderer supports adaptation between supported tracks in a\n     * specified {@link TrackGroup}.\n     *\n     * <p>Tracks for which {@link #getTrackSupport(int, int, int)} returns {@link\n     * RendererCapabilities#FORMAT_HANDLED} are always considered. Tracks for which {@link\n     * #getTrackSupport(int, int, int)} returns {@link\n     * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are also considered if {@code\n     * includeCapabilitiesExceededTracks} is set to {@code true}. Tracks for which {@link\n     * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},\n     * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or {@link\n     * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groupIndex The index of the track group.\n     * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the\n     *     renderer are included when determining support.\n     * @return The {@link AdaptiveSupport}.\n     */\n    @AdaptiveSupport\n    public int getAdaptiveSupport(\n        int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) {\n      int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length;\n      // Iterate over the tracks in the group, recording the indices of those to consider.\n      int[] trackIndices = new int[trackCount];\n      int trackIndexCount = 0;\n      for (int i = 0; i < trackCount; i++) {\n        @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i);\n        if (fixedSupport == RendererCapabilities.FORMAT_HANDLED\n            || (includeCapabilitiesExceededTracks\n            && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {\n          trackIndices[trackIndexCount++] = i;\n        }\n      }\n      trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);\n      return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);\n    }\n\n    /**\n     * Returns the extent to which a renderer supports adaptation between specified tracks within a\n     * {@link TrackGroup}.\n     *\n     * @param rendererIndex The renderer index.\n     * @param groupIndex The index of the track group.\n     * @return The {@link AdaptiveSupport}.\n     */\n    @AdaptiveSupport\n    public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {\n      int handledTrackCount = 0;\n      @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;\n      boolean multipleMimeTypes = false;\n      String firstSampleMimeType = null;\n      for (int i = 0; i < trackIndices.length; i++) {\n        int trackIndex = trackIndices[i];\n        String sampleMimeType =\n            rendererTrackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex).sampleMimeType;\n        if (handledTrackCount++ == 0) {\n          firstSampleMimeType = sampleMimeType;\n        } else {\n          multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);\n        }\n        adaptiveSupport =\n            Math.min(\n                adaptiveSupport,\n                RendererCapabilities.getAdaptiveSupport(\n                    rendererFormatSupports[rendererIndex][groupIndex][i]));\n      }\n      return multipleMimeTypes\n          ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex])\n          : adaptiveSupport;\n    }\n\n    /** @deprecated Use {@link #getUnmappedTrackGroups()}. */\n    @Deprecated\n    public TrackGroupArray getUnassociatedTrackGroups() {\n      return getUnmappedTrackGroups();\n    }\n\n    /** Returns {@link TrackGroup}s not mapped to any renderer. */\n    public TrackGroupArray getUnmappedTrackGroups() {\n      return unmappedTrackGroups;\n    }\n\n  }\n\n  @Nullable private MappedTrackInfo currentMappedTrackInfo;\n\n  /**\n   * Returns the mapping information for the currently active track selection, or null if no\n   * selection is currently active.\n   */\n  public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {\n    return currentMappedTrackInfo;\n  }\n\n  // TrackSelector implementation.\n\n  @Override\n  public final void onSelectionActivated(Object info) {\n    currentMappedTrackInfo = (MappedTrackInfo) info;\n  }\n\n  @Override\n  public final TrackSelectorResult selectTracks(\n      RendererCapabilities[] rendererCapabilities,\n      TrackGroupArray trackGroups,\n      MediaPeriodId periodId,\n      Timeline timeline)\n      throws ExoPlaybackException {\n    // Structures into which data will be written during the selection. The extra item at the end\n    // of each array is to store data associated with track groups that cannot be associated with\n    // any renderer.\n    int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1];\n    TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][];\n    @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][];\n    for (int i = 0; i < rendererTrackGroups.length; i++) {\n      rendererTrackGroups[i] = new TrackGroup[trackGroups.length];\n      rendererFormatSupports[i] = new int[trackGroups.length][];\n    }\n\n    // Determine the extent to which each renderer supports mixed mimeType adaptation.\n    @AdaptiveSupport\n    int[] rendererMixedMimeTypeAdaptationSupports =\n        getMixedMimeTypeAdaptationSupports(rendererCapabilities);\n\n    // Associate each track group to a preferred renderer, and evaluate the support that the\n    // renderer provides for each track in the group.\n    for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {\n      TrackGroup group = trackGroups.get(groupIndex);\n      // Associate the group to a preferred renderer.\n      int rendererIndex = findRenderer(rendererCapabilities, group);\n      // Evaluate the support that the renderer provides for each track in the group.\n      @Capabilities\n      int[] rendererFormatSupport =\n          rendererIndex == rendererCapabilities.length\n              ? new int[group.length]\n              : getFormatSupport(rendererCapabilities[rendererIndex], group);\n      // Stash the results.\n      int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex];\n      rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group;\n      rendererFormatSupports[rendererIndex][rendererTrackGroupCount] = rendererFormatSupport;\n      rendererTrackGroupCounts[rendererIndex]++;\n    }\n\n    // Create a track group array for each renderer, and trim each rendererFormatSupports entry.\n    TrackGroupArray[] rendererTrackGroupArrays = new TrackGroupArray[rendererCapabilities.length];\n    int[] rendererTrackTypes = new int[rendererCapabilities.length];\n    for (int i = 0; i < rendererCapabilities.length; i++) {\n      int rendererTrackGroupCount = rendererTrackGroupCounts[i];\n      rendererTrackGroupArrays[i] =\n          new TrackGroupArray(\n              Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount));\n      rendererFormatSupports[i] =\n          Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount);\n      rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();\n    }\n\n    // Create a track group array for track groups not mapped to a renderer.\n    int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];\n    TrackGroupArray unmappedTrackGroupArray =\n        new TrackGroupArray(\n            Util.nullSafeArrayCopy(\n                rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount));\n\n    // Package up the track information and selections.\n    MappedTrackInfo mappedTrackInfo =\n        new MappedTrackInfo(\n            rendererTrackTypes,\n            rendererTrackGroupArrays,\n            rendererMixedMimeTypeAdaptationSupports,\n            rendererFormatSupports,\n            unmappedTrackGroupArray);\n\n    Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result =\n        selectTracks(\n            mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports);\n    return new TrackSelectorResult(result.first, result.second, mappedTrackInfo);\n  }\n\n  /**\n   * Given mapped track information, returns a track selection and configuration for each renderer.\n   *\n   * @param mappedTrackInfo Mapped track information.\n   * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by\n   *     renderer, track group and track (in that order).\n   * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type\n   *     adaptation for the renderer.\n   * @return A pair consisting of the track selections and configurations for each renderer. A null\n   *     configuration indicates the renderer should be disabled, in which case the track selection\n   *     will also be null. A track selection may also be null for a non-disabled renderer if {@link\n   *     RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}.\n   * @throws ExoPlaybackException If an error occurs while selecting the tracks.\n   */\n  protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>\n      selectTracks(\n          MappedTrackInfo mappedTrackInfo,\n          @Capabilities int[][][] rendererFormatSupports,\n          @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport)\n          throws ExoPlaybackException;\n\n  /**\n   * Finds the renderer to which the provided {@link TrackGroup} should be mapped.\n   * <p>\n   * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in\n   * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED},\n   * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},\n   * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and\n   * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers\n   * report the same level of support, the renderer with the lowest index is associated.\n   * <p>\n   * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the\n   * tracks in the group, then {@code renderers.length} is returned to indicate that the group was\n   * not mapped to any renderer.\n   *\n   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.\n   * @param group The track group to map to a renderer.\n   * @return The index of the renderer to which the track group was mapped, or\n   *     {@code renderers.length} if it was not mapped to any renderer.\n   * @throws ExoPlaybackException If an error occurs finding a renderer.\n   */\n  private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group)\n      throws ExoPlaybackException {\n    int bestRendererIndex = rendererCapabilities.length;\n    @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;\n    for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) {\n      RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex];\n      for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {\n        @FormatSupport\n        int formatSupportLevel =\n            RendererCapabilities.getFormatSupport(\n                rendererCapability.supportsFormat(group.getFormat(trackIndex)));\n        if (formatSupportLevel > bestFormatSupportLevel) {\n          bestRendererIndex = rendererIndex;\n          bestFormatSupportLevel = formatSupportLevel;\n          if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) {\n            // We can't do better.\n            return bestRendererIndex;\n          }\n        }\n      }\n    }\n    return bestRendererIndex;\n  }\n\n  /**\n   * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link\n   * TrackGroup}, returning the results in an array.\n   *\n   * @param rendererCapabilities The {@link RendererCapabilities} of the renderer.\n   * @param group The track group to evaluate.\n   * @return An array containing {@link Capabilities} for each track in the group.\n   * @throws ExoPlaybackException If an error occurs determining the format support.\n   */\n  @Capabilities\n  private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group)\n      throws ExoPlaybackException {\n    @Capabilities int[] formatSupport = new int[group.length];\n    for (int i = 0; i < group.length; i++) {\n      formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i));\n    }\n    return formatSupport;\n  }\n\n  /**\n   * Calls {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer,\n   * returning the results in an array.\n   *\n   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.\n   * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the\n   *     renderer.\n   * @throws ExoPlaybackException If an error occurs determining the adaptation support.\n   */\n  @AdaptiveSupport\n  private static int[] getMixedMimeTypeAdaptationSupports(\n      RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {\n    @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length];\n    for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) {\n      mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation();\n    }\n    return mixedMimeTypeAdaptationSupport;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport java.util.List;\nimport java.util.Random;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A {@link TrackSelection} whose selected track is updated randomly.\n */\npublic final class RandomTrackSelection extends BaseTrackSelection {\n\n  /**\n   * Factory for {@link RandomTrackSelection} instances.\n   */\n  public static final class Factory implements TrackSelection.Factory {\n\n    private final Random random;\n\n    public Factory() {\n      random = new Random();\n    }\n\n    /**\n     * @param seed A seed for the {@link Random} instance used by the factory.\n     */\n    public Factory(int seed) {\n      random = new Random(seed);\n    }\n\n    @Override\n    public @NullableType TrackSelection[] createTrackSelections(\n        @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n      return TrackSelectionUtil.createTrackSelectionsForDefinitions(\n          definitions,\n          definition -> new RandomTrackSelection(definition.group, definition.tracks, random));\n    }\n  }\n\n  private final Random random;\n\n  private int selectedIndex;\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     null or empty. May be in any order.\n   */\n  public RandomTrackSelection(TrackGroup group, int... tracks) {\n    super(group, tracks);\n    random = new Random();\n    selectedIndex = random.nextInt(length);\n  }\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     null or empty. May be in any order.\n   * @param seed A seed for the {@link Random} instance used to update the selected track.\n   */\n  public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) {\n    this(group, tracks, new Random(seed));\n  }\n\n  /**\n   * @param group The {@link TrackGroup}. Must not be null.\n   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n   *     null or empty. May be in any order.\n   * @param random A source of random numbers.\n   */\n  public RandomTrackSelection(TrackGroup group, int[] tracks, Random random) {\n    super(group, tracks);\n    this.random = random;\n    selectedIndex = random.nextInt(length);\n  }\n\n  @Override\n  public void updateSelectedTrack(\n      long playbackPositionUs,\n      long bufferedDurationUs,\n      long availableDurationUs,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] mediaChunkIterators) {\n    // Count the number of non-blacklisted formats.\n    long nowMs = SystemClock.elapsedRealtime();\n    int nonBlacklistedFormatCount = 0;\n    for (int i = 0; i < length; i++) {\n      if (!isBlacklisted(i, nowMs)) {\n        nonBlacklistedFormatCount++;\n      }\n    }\n\n    selectedIndex = random.nextInt(nonBlacklistedFormatCount);\n    if (nonBlacklistedFormatCount != length) {\n      // Adjust the format index to account for blacklisted formats.\n      nonBlacklistedFormatCount = 0;\n      for (int i = 0; i < length; i++) {\n        if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) {\n          selectedIndex = i;\n          return;\n        }\n      }\n    }\n  }\n\n  @Override\n  public int getSelectedIndex() {\n    return selectedIndex;\n  }\n\n  @Override\n  public int getSelectionReason() {\n    return C.SELECTION_REASON_ADAPTIVE;\n  }\n\n  @Override\n  @Nullable\n  public Object getSelectionData() {\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * A track selection consisting of a static subset of selected tracks belonging to a {@link\n * TrackGroup}, and a possibly varying individual selected track from the subset.\n *\n * <p>Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual\n * selected track may change dynamically as a result of calling {@link #updateSelectedTrack(long,\n * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)}. This only\n * happens between calls to {@link #enable()} and {@link #disable()}.\n */\npublic interface TrackSelection {\n\n  /** Contains of a subset of selected tracks belonging to a {@link TrackGroup}. */\n  final class Definition {\n    /** The {@link TrackGroup} which tracks belong to. */\n    public final TrackGroup group;\n    /** The indices of the selected tracks in {@link #group}. */\n    public final int[] tracks;\n    /** The track selection reason. One of the {@link C} SELECTION_REASON_ constants. */\n    public final int reason;\n    /** Optional data associated with this selection of tracks. */\n    @Nullable public final Object data;\n\n    /**\n     * @param group The {@link TrackGroup}. Must not be null.\n     * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n     *     null or empty. May be in any order.\n     */\n    public Definition(TrackGroup group, int... tracks) {\n      this(group, tracks, C.SELECTION_REASON_UNKNOWN, /* data= */ null);\n    }\n\n    /**\n     * @param group The {@link TrackGroup}. Must not be null.\n     * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\n     * @param reason The track selection reason. One of the {@link C} SELECTION_REASON_ constants.\n     * @param data Optional data associated with this selection of tracks.\n     */\n    public Definition(TrackGroup group, int[] tracks, int reason, @Nullable Object data) {\n      this.group = group;\n      this.tracks = tracks;\n      this.reason = reason;\n      this.data = data;\n    }\n  }\n\n  /**\n   * Factory for {@link TrackSelection} instances.\n   */\n  interface Factory {\n\n    /**\n     * Creates track selections for the provided {@link Definition Definitions}.\n     *\n     * <p>Implementations that create at most one adaptive track selection may use {@link\n     * TrackSelectionUtil#createTrackSelectionsForDefinitions}.\n     *\n     * @param definitions A {@link Definition} array. May include null values.\n     * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.\n     * @return The created selections. Must have the same length as {@code definitions} and may\n     *     include null values.\n     */\n    @NullableType\n    TrackSelection[] createTrackSelections(\n            @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter);\n  }\n\n  /**\n   * Enables the track selection. Dynamic changes via {@link #updateSelectedTrack(long, long, long,\n   * List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will only happen after\n   * this call.\n   *\n   * <p>This method may not be called when the track selection is already enabled.\n   */\n  void enable();\n\n  /**\n   * Disables this track selection. No further dynamic changes via {@link #updateSelectedTrack(long,\n   * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will happen\n   * after this call.\n   *\n   * <p>This method may only be called when the track selection is already enabled.\n   */\n  void disable();\n\n  /**\n   * Returns the {@link TrackGroup} to which the selected tracks belong.\n   */\n  TrackGroup getTrackGroup();\n\n  // Static subset of selected tracks.\n\n  /**\n   * Returns the number of tracks in the selection.\n   */\n  int length();\n\n  /**\n   * Returns the format of the track at a given index in the selection.\n   *\n   * @param index The index in the selection.\n   * @return The format of the selected track.\n   */\n  Format getFormat(int index);\n\n  /**\n   * Returns the index in the track group of the track at a given index in the selection.\n   *\n   * @param index The index in the selection.\n   * @return The index of the selected track.\n   */\n  int getIndexInTrackGroup(int index);\n\n  /**\n   * Returns the index in the selection of the track with the specified format. The format is\n   * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) ==\n   * index} even if multiple selected tracks have formats that contain the same values.\n   *\n   * @param format The format.\n   * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified\n   *     format is not part of the selection.\n   */\n  int indexOf(Format format);\n\n  /**\n   * Returns the index in the selection of the track with the specified index in the track group.\n   *\n   * @param indexInTrackGroup The index in the track group.\n   * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified\n   *     index is not part of the selection.\n   */\n  int indexOf(int indexInTrackGroup);\n\n  // Individual selected track.\n\n  /**\n   * Returns the {@link Format} of the individual selected track.\n   */\n  Format getSelectedFormat();\n\n  /**\n   * Returns the index in the track group of the individual selected track.\n   */\n  int getSelectedIndexInTrackGroup();\n\n  /**\n   * Returns the index of the selected track.\n   */\n  int getSelectedIndex();\n\n  /**\n   * Returns the reason for the current track selection.\n   */\n  int getSelectionReason();\n\n  /** Returns optional data associated with the current track selection. */\n  @Nullable Object getSelectionData();\n\n  // Adaptation.\n\n  /**\n   * Called to notify the selection of the current playback speed. The playback speed may affect\n   * adaptive track selection.\n   *\n   * @param speed The playback speed.\n   */\n  void onPlaybackSpeed(float speed);\n\n  /**\n   * Called to notify the selection of a position discontinuity.\n   *\n   * <p>This happens when the playback position jumps, e.g., as a result of a seek being performed.\n   */\n  default void onDiscontinuity() {}\n\n  /**\n   * Updates the selected track for sources that load media in discrete {@link MediaChunk}s.\n   *\n   * <p>This method may only be called when the selection is enabled.\n   *\n   * @param playbackPositionUs The current playback position in microseconds. If playback of the\n   *     period to which this track selection belongs has not yet started, the value will be the\n   *     starting position in the period minus the duration of any media in previous periods still\n   *     to be played.\n   * @param bufferedDurationUs The duration of media currently buffered from the current playback\n   *     position, in microseconds. Note that the next load position can be calculated as {@code\n   *     (playbackPositionUs + bufferedDurationUs)}.\n   * @param availableDurationUs The duration of media available for buffering from the current\n   *     playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered to the\n   *     end of the current period. Note that if not set to {@link C#TIME_UNSET}, the position up to\n   *     which media is available for buffering can be calculated as {@code (playbackPositionUs +\n   *     availableDurationUs)}.\n   * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified.\n   * @param mediaChunkIterators An array of {@link MediaChunkIterator}s providing information about\n   *     the sequence of upcoming media chunks for each track in the selection. All iterators start\n   *     from the media chunk which will be loaded next if the respective track is selected. Note\n   *     that this information may not be available for all tracks, and so some iterators may be\n   *     empty.\n   */\n  void updateSelectedTrack(\n          long playbackPositionUs,\n          long bufferedDurationUs,\n          long availableDurationUs,\n          List<? extends MediaChunk> queue,\n          MediaChunkIterator[] mediaChunkIterators);\n\n  /**\n   * May be called periodically by sources that load media in discrete {@link MediaChunk}s and\n   * support discarding of buffered chunks in order to re-buffer using a different selected track.\n   * Returns the number of chunks that should be retained in the queue.\n   * <p>\n   * To avoid excessive re-buffering, implementations should normally return the size of the queue.\n   * An example of a case where a smaller value may be returned is if network conditions have\n   * improved dramatically, allowing chunks to be discarded and re-buffered in a track of\n   * significantly higher quality. Discarding chunks may allow faster switching to a higher quality\n   * track in this case. This method may only be called when the selection is enabled.\n   *\n   * @param playbackPositionUs The current playback position in microseconds. If playback of the\n   *     period to which this track selection belongs has not yet started, the value will be the\n   *     starting position in the period minus the duration of any media in previous periods still\n   *     to be played.\n   * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified.\n   * @return The number of chunks to retain in the queue.\n   */\n  int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);\n\n  /**\n   * Attempts to blacklist the track at the specified index in the selection, making it ineligible\n   * for selection by calls to {@link #updateSelectedTrack(long, long, long, List,\n   * MediaChunkIterator[])} for the specified period of time. Blacklisting will fail if all other\n   * tracks are currently blacklisted. If blacklisting the currently selected track, note that it\n   * will remain selected until the next call to {@link #updateSelectedTrack(long, long, long, List,\n   * MediaChunkIterator[])}.\n   *\n   * <p>This method may only be called when the selection is enabled.\n   *\n   * @param index The index of the track in the selection.\n   * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in\n   *     milliseconds.\n   * @return Whether blacklisting was successful.\n   */\n  boolean blacklist(int index, long blacklistDurationMs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** An array of {@link TrackSelection}s. */\npublic final class TrackSelectionArray {\n\n  /** The length of this array. */\n  public final int length;\n\n  private final @NullableType TrackSelection[] trackSelections;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /** @param trackSelections The selections. Must not be null, but may contain null elements. */\n  public TrackSelectionArray(@NullableType TrackSelection... trackSelections) {\n    this.trackSelections = trackSelections;\n    this.length = trackSelections.length;\n  }\n\n  /**\n   * Returns the selection at a given index.\n   *\n   * @param index The index of the selection.\n   * @return The selection.\n   */\n  @Nullable\n  public TrackSelection get(int index) {\n    return trackSelections[index];\n  }\n\n  /** Returns the selections in a newly allocated array. */\n  public @NullableType TrackSelection[] getAll() {\n    return trackSelections.clone();\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 17;\n      result = 31 * result + Arrays.hashCode(trackSelections);\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    TrackSelectionArray other = (TrackSelectionArray) obj;\n    return Arrays.equals(trackSelections, other.trackSelections);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.os.Looper;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\nimport android.view.accessibility.CaptioningManager;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Locale;\n\n/** Constraint parameters for track selection. */\npublic class TrackSelectionParameters implements Parcelable {\n\n  /**\n   * A builder for {@link TrackSelectionParameters}. See the {@link TrackSelectionParameters}\n   * documentation for explanations of the parameters that can be configured using this builder.\n   */\n  public static class Builder {\n\n    @Nullable /* package */ String preferredAudioLanguage;\n    @Nullable /* package */ String preferredTextLanguage;\n    @C.RoleFlags /* package */ int preferredTextRoleFlags;\n    /* package */ boolean selectUndeterminedTextLanguage;\n    @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags;\n\n    /**\n     * Creates a builder with default initial values.\n     *\n     * @param context Any context.\n     */\n    @SuppressWarnings({\"deprecation\", \"initialization:method.invocation.invalid\"})\n    public Builder(Context context) {\n      this();\n      setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);\n    }\n\n    /**\n     * @deprecated {@link Context} constraints will not be set when using this constructor. Use\n     *     {@link #Builder(Context)} instead.\n     */\n    @Deprecated\n    public Builder() {\n      preferredAudioLanguage = null;\n      preferredTextLanguage = null;\n      preferredTextRoleFlags = 0;\n      selectUndeterminedTextLanguage = false;\n      disabledTextTrackSelectionFlags = 0;\n    }\n\n    /**\n     * @param initialValues The {@link TrackSelectionParameters} from which the initial values of\n     *     the builder are obtained.\n     */\n    /* package */ Builder(TrackSelectionParameters initialValues) {\n      preferredAudioLanguage = initialValues.preferredAudioLanguage;\n      preferredTextLanguage = initialValues.preferredTextLanguage;\n      preferredTextRoleFlags = initialValues.preferredTextRoleFlags;\n      selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage;\n      disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags;\n    }\n\n    /**\n     * Sets the preferred language for audio and forced text tracks.\n     *\n     * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or\n     *     {@code null} to select the default track, or the first track if there's no default.\n     * @return This builder.\n     */\n    public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {\n      this.preferredAudioLanguage = preferredAudioLanguage;\n      return this;\n    }\n\n    /**\n     * Sets the preferred language and role flags for text tracks based on the accessibility\n     * settings of {@link CaptioningManager}.\n     *\n     * <p>Does nothing for API levels &lt; 19 or when the {@link CaptioningManager} is disabled.\n     *\n     * @param context A {@link Context}.\n     * @return This builder.\n     */\n    public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(\n        Context context) {\n      if (Util.SDK_INT >= 19) {\n        setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(context);\n      }\n      return this;\n    }\n\n    /**\n     * Sets the preferred language for text tracks.\n     *\n     * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or\n     *     {@code null} to select the default track if there is one, or no track otherwise.\n     * @return This builder.\n     */\n    public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {\n      this.preferredTextLanguage = preferredTextLanguage;\n      return this;\n    }\n\n    /**\n     * Sets the preferred {@link C.RoleFlags} for text tracks.\n     *\n     * @param preferredTextRoleFlags Preferred text role flags.\n     * @return This builder.\n     */\n    public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {\n      this.preferredTextRoleFlags = preferredTextRoleFlags;\n      return this;\n    }\n\n    /**\n     * Sets whether a text track with undetermined language should be selected if no track with\n     * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is\n     * unset.\n     *\n     * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should\n     *     be selected if no preferred language track is available.\n     * @return This builder.\n     */\n    public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) {\n      this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;\n      return this;\n    }\n\n    /**\n     * Sets a bitmask of selection flags that are disabled for text track selections.\n     *\n     * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are\n     *     disabled for text track selections.\n     * @return This builder.\n     */\n    public Builder setDisabledTextTrackSelectionFlags(\n        @C.SelectionFlags int disabledTextTrackSelectionFlags) {\n      this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;\n      return this;\n    }\n\n    /** Builds a {@link TrackSelectionParameters} instance with the selected values. */\n    public TrackSelectionParameters build() {\n      return new TrackSelectionParameters(\n          // Audio\n          preferredAudioLanguage,\n          // Text\n          preferredTextLanguage,\n          preferredTextRoleFlags,\n          selectUndeterminedTextLanguage,\n          disabledTextTrackSelectionFlags);\n    }\n\n    @TargetApi(19)\n    private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(\n        Context context) {\n      if (Util.SDK_INT < 23 && Looper.myLooper() == null) {\n        // Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when\n        // CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904].\n        return;\n      }\n      CaptioningManager captioningManager =\n          (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);\n      if (captioningManager == null || !captioningManager.isEnabled()) {\n        return;\n      }\n      preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;\n      Locale preferredLocale = captioningManager.getLocale();\n      if (preferredLocale != null) {\n        preferredTextLanguage = Util.getLocaleLanguageTag(preferredLocale);\n      }\n    }\n  }\n\n  /**\n   * An instance with default values, except those obtained from the {@link Context}.\n   *\n   * <p>If possible, use {@link #getDefaults(Context)} instead.\n   *\n   * <p>This instance will not have the following settings:\n   *\n   * <ul>\n   *   <li>{@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)\n   *       Preferred text language and role flags} configured to the accessibility settings of\n   *       {@link CaptioningManager}.\n   * </ul>\n   */\n  @SuppressWarnings(\"deprecation\")\n  public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = new Builder().build();\n\n  /**\n   * @deprecated This instance is not configured using {@link Context} constraints. Use {@link\n   *     #getDefaults(Context)} instead.\n   */\n  @Deprecated public static final TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;\n\n  /** Returns an instance configured with default values. */\n  public static TrackSelectionParameters getDefaults(Context context) {\n    return new Builder(context).build();\n  }\n\n  /**\n   * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag.\n   * {@code null} selects the default track, or the first track if there's no default. The default\n   * value is {@code null}.\n   */\n  @Nullable public final String preferredAudioLanguage;\n  /**\n   * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects\n   * the default track if there is one, or no track otherwise. The default value is {@code null}, or\n   * the language of the accessibility {@link CaptioningManager} if enabled.\n   */\n  @Nullable public final String preferredTextLanguage;\n  /**\n   * The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there\n   * is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE}\n   * | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager}\n   * is enabled.\n   */\n  @C.RoleFlags public final int preferredTextRoleFlags;\n  /**\n   * Whether a text track with undetermined language should be selected if no track with {@link\n   * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The\n   * default value is {@code false}.\n   */\n  public final boolean selectUndeterminedTextLanguage;\n  /**\n   * Bitmask of selection flags that are disabled for text track selections. See {@link\n   * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).\n   */\n  @C.SelectionFlags public final int disabledTextTrackSelectionFlags;\n\n  /* package */ TrackSelectionParameters(\n      @Nullable String preferredAudioLanguage,\n      @Nullable String preferredTextLanguage,\n      @C.RoleFlags int preferredTextRoleFlags,\n      boolean selectUndeterminedTextLanguage,\n      @C.SelectionFlags int disabledTextTrackSelectionFlags) {\n    // Audio\n    this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage);\n    // Text\n    this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage);\n    this.preferredTextRoleFlags = preferredTextRoleFlags;\n    this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;\n    this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;\n  }\n\n  /* package */ TrackSelectionParameters(Parcel in) {\n    this.preferredAudioLanguage = in.readString();\n    this.preferredTextLanguage = in.readString();\n    this.preferredTextRoleFlags = in.readInt();\n    this.selectUndeterminedTextLanguage = Util.readBoolean(in);\n    this.disabledTextTrackSelectionFlags = in.readInt();\n  }\n\n  /** Creates a new {@link Builder}, copying the initial values from this instance. */\n  public Builder buildUpon() {\n    return new Builder(this);\n  }\n\n  @Override\n  @SuppressWarnings(\"EqualsGetClass\")\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    TrackSelectionParameters other = (TrackSelectionParameters) obj;\n    return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage)\n        && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage)\n        && preferredTextRoleFlags == other.preferredTextRoleFlags\n        && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage\n        && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = 1;\n    result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode());\n    result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode());\n    result = 31 * result + preferredTextRoleFlags;\n    result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);\n    result = 31 * result + disabledTextTrackSelectionFlags;\n    return result;\n  }\n\n  // Parcelable implementation.\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeString(preferredAudioLanguage);\n    dest.writeString(preferredTextLanguage);\n    dest.writeInt(preferredTextRoleFlags);\n    Util.writeBoolean(dest, selectUndeterminedTextLanguage);\n    dest.writeInt(disabledTextTrackSelectionFlags);\n  }\n\n  public static final Creator<TrackSelectionParameters> CREATOR =\n      new Creator<TrackSelectionParameters>() {\n\n        @Override\n        public TrackSelectionParameters createFromParcel(Parcel in) {\n          return new TrackSelectionParameters(in);\n        }\n\n        @Override\n        public TrackSelectionParameters[] newArray(int size) {\n          return new TrackSelectionParameters[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** Track selection related utility methods. */\npublic final class TrackSelectionUtil {\n\n  private TrackSelectionUtil() {}\n\n  /** Functional interface to create a single adaptive track selection. */\n  public interface AdaptiveTrackSelectionFactory {\n\n    /**\n     * Creates an adaptive track selection for the provided track selection definition.\n     *\n     * @param trackSelectionDefinition A {@link Definition} for the track selection.\n     * @return The created track selection.\n     */\n    TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);\n  }\n\n  /**\n   * Creates track selections for an array of track selection definitions, with at most one\n   * multi-track adaptive selection.\n   *\n   * @param definitions The list of track selection {@link Definition definitions}. May include null\n   *     values.\n   * @param adaptiveTrackSelectionFactory A factory for the multi-track adaptive track selection.\n   * @return The array of created track selection. For null entries in {@code definitions} returns\n   *     null values.\n   */\n  public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions(\n      @NullableType Definition[] definitions,\n      AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) {\n    TrackSelection[] selections = new TrackSelection[definitions.length];\n    boolean createdAdaptiveTrackSelection = false;\n    for (int i = 0; i < definitions.length; i++) {\n      Definition definition = definitions[i];\n      if (definition == null) {\n        continue;\n      }\n      if (definition.tracks.length > 1 && !createdAdaptiveTrackSelection) {\n        createdAdaptiveTrackSelection = true;\n        selections[i] = adaptiveTrackSelectionFactory.createAdaptiveTrackSelection(definition);\n      } else {\n        selections[i] =\n            new FixedTrackSelection(\n                definition.group, definition.tracks[0], definition.reason, definition.data);\n      }\n    }\n    return selections;\n  }\n\n  /**\n   * Updates {@link DefaultTrackSelector.Parameters} with an override.\n   *\n   * @param parameters The current {@link DefaultTrackSelector.Parameters} to build upon.\n   * @param rendererIndex The renderer index to update.\n   * @param trackGroupArray The {@link TrackGroupArray} of the renderer.\n   * @param isDisabled Whether the renderer should be set disabled.\n   * @param override An optional override for the renderer. If null, no override will be set and an\n   *     existing override for this renderer will be cleared.\n   * @return The updated {@link DefaultTrackSelector.Parameters}.\n   */\n  public static DefaultTrackSelector.Parameters updateParametersWithOverride(\n      DefaultTrackSelector.Parameters parameters,\n      int rendererIndex,\n      TrackGroupArray trackGroupArray,\n      boolean isDisabled,\n      @Nullable SelectionOverride override) {\n    DefaultTrackSelector.ParametersBuilder builder =\n        parameters\n            .buildUpon()\n            .clearSelectionOverrides(rendererIndex)\n            .setRendererDisabled(rendererIndex, isDisabled);\n    if (override != null) {\n      builder.setSelectionOverride(rendererIndex, trackGroupArray, override);\n    }\n    return builder.build();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RendererConfiguration;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of\n * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be\n * suitable for most use cases.\n *\n * <h3>Interactions with the player</h3>\n *\n * The following interactions occur between the player and its track selector during playback.\n *\n * <p>\n *\n * <ul>\n *   <li>When the player is created it will initialize the track selector by calling {@link\n *       #init(InvalidationListener, BandwidthMeter)}.\n *   <li>When the player needs to make a track selection it will call {@link\n *       #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}. This\n *       typically occurs at the start of playback, when the player starts to buffer a new period of\n *       the media being played, and when the track selector invalidates its previous selections.\n *   <li>The player may perform a track selection well in advance of the selected tracks becoming\n *       active, where active is defined to mean that the renderers are actually consuming media\n *       corresponding to the selection that was made. For example when playing media containing\n *       multiple periods, the track selection for a period is made when the player starts to buffer\n *       that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the\n *       selection will occur approximately 30 seconds in advance of it becoming active. In fact the\n *       selection may never become active, for example if the user seeks to some other period of\n *       the media during the 30 second gap. The player indicates to the track selector when a\n *       selection it has previously made becomes active by calling {@link\n *       #onSelectionActivated(Object)}.\n *   <li>If the track selector wishes to indicate to the player that selections it has previously\n *       made are invalid, it can do so by calling {@link\n *       InvalidationListener#onTrackSelectionsInvalidated()} on the {@link InvalidationListener}\n *       that was passed to {@link #init(InvalidationListener, BandwidthMeter)}. A track selector\n *       may wish to do this if its configuration has changed, for example if it now wishes to\n *       prefer audio tracks in a particular language. This will trigger the player to make new\n *       track selections. Note that the player will have to re-buffer in the case that the new\n *       track selection for the currently playing period differs from the one that was invalidated.\n * </ul>\n *\n * <h3>Renderer configuration</h3>\n *\n * The {@link TrackSelectorResult} returned by {@link #selectTracks(RendererCapabilities[],\n * TrackGroupArray, MediaPeriodId, Timeline)} contains not only {@link TrackSelection}s for each\n * renderer, but also {@link RendererConfiguration}s defining configuration parameters that the\n * renderers should apply when consuming the corresponding media. Whilst it may seem counter-\n * intuitive for a track selector to also specify renderer configuration information, in practice\n * the two are tightly bound together. It may only be possible to play a certain combination tracks\n * if the renderers are configured in a particular way. Equally, it may only be possible to\n * configure renderers in a particular way if certain tracks are selected. Hence it makes sense to\n * determine the track selection and corresponding renderer configurations in a single step.\n *\n * <h3>Threading model</h3>\n *\n * All calls made by the player into the track selector are on the player's internal playback\n * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()}\n * from any thread.\n */\npublic abstract class TrackSelector {\n\n  /**\n   * Notified when selections previously made by a {@link TrackSelector} are no longer valid.\n   */\n  public interface InvalidationListener {\n\n    /**\n     * Called by a {@link TrackSelector} to indicate that selections it has previously made are no\n     * longer valid. May be called from any thread.\n     */\n    void onTrackSelectionsInvalidated();\n\n  }\n\n  @Nullable private InvalidationListener listener;\n  @Nullable private BandwidthMeter bandwidthMeter;\n\n  /**\n   * Called by the player to initialize the selector.\n   *\n   * @param listener An invalidation listener that the selector can call to indicate that selections\n   *     it has previously made are no longer valid.\n   * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks.\n   */\n  public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) {\n    this.listener = listener;\n    this.bandwidthMeter = bandwidthMeter;\n  }\n\n  /**\n   * Called by the player to perform a track selection.\n   *\n   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks\n   *     are to be selected.\n   * @param trackGroups The available track groups.\n   * @param periodId The {@link MediaPeriodId} of the period for which tracks are to be selected.\n   * @param timeline The {@link Timeline} holding the period for which tracks are to be selected.\n   * @return A {@link TrackSelectorResult} describing the track selections.\n   * @throws ExoPlaybackException If an error occurs selecting tracks.\n   */\n  public abstract TrackSelectorResult selectTracks(\n      RendererCapabilities[] rendererCapabilities,\n      TrackGroupArray trackGroups,\n      MediaPeriodId periodId,\n      Timeline timeline)\n      throws ExoPlaybackException;\n\n  /**\n   * Called by the player when a {@link TrackSelectorResult} previously generated by {@link\n   * #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} is activated.\n   *\n   * @param info The value of {@link TrackSelectorResult#info} in the activated selection.\n   */\n  public abstract void onSelectionActivated(Object info);\n\n  /**\n   * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously\n   * generated track selections.\n   */\n  protected final void invalidate() {\n    if (listener != null) {\n      listener.onTrackSelectionsInvalidated();\n    }\n  }\n\n  /**\n   * Returns a bandwidth meter which can be used by track selections to select tracks. Must only be\n   * called after {@link #init(InvalidationListener, BandwidthMeter)} has been called.\n   */\n  protected final BandwidthMeter getBandwidthMeter() {\n    return Assertions.checkNotNull(bandwidthMeter);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.RendererConfiguration;\nimport com.google.android.exoplayer2.util.Util;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/**\n * The result of a {@link TrackSelector} operation.\n */\npublic final class TrackSelectorResult {\n\n  /** The number of selections in the result. Greater than or equal to zero. */\n  public final int length;\n  /**\n   * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding\n   * renderer should be disabled.\n   */\n  public final @NullableType RendererConfiguration[] rendererConfigurations;\n  /**\n   * A {@link TrackSelectionArray} containing the track selection for each renderer.\n   */\n  public final TrackSelectionArray selections;\n  /**\n   * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}\n   * should the selections be activated.\n   */\n  public final Object info;\n\n  /**\n   * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry\n   *     indicates the corresponding renderer should be disabled.\n   * @param selections A {@link TrackSelectionArray} containing the selection for each renderer.\n   * @param info An opaque object that will be returned to {@link\n   *     TrackSelector#onSelectionActivated(Object)} should the selection be activated.\n   */\n  public TrackSelectorResult(\n      @NullableType RendererConfiguration[] rendererConfigurations,\n      @NullableType TrackSelection[] selections,\n      Object info) {\n    this.rendererConfigurations = rendererConfigurations;\n    this.selections = new TrackSelectionArray(selections);\n    this.info = info;\n    length = rendererConfigurations.length;\n  }\n\n  /** Returns whether the renderer at the specified index is enabled. */\n  public boolean isRendererEnabled(int index) {\n    return rendererConfigurations[index] != null;\n  }\n\n  /**\n   * Returns whether this result is equivalent to {@code other} for all renderers.\n   *\n   * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}\n   *     will be returned.\n   * @return Whether this result is equivalent to {@code other} for all renderers.\n   */\n  public boolean isEquivalent(@Nullable TrackSelectorResult other) {\n    if (other == null || other.selections.length != selections.length) {\n      return false;\n    }\n    for (int i = 0; i < selections.length; i++) {\n      if (!isEquivalent(other, i)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Returns whether this result is equivalent to {@code other} for the renderer at the given index.\n   * The results are equivalent if they have equal track selections and configurations for the\n   * renderer.\n   *\n   * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}\n   *     will be returned.\n   * @param index The renderer index to check for equivalence.\n   * @return Whether this result is equivalent to {@code other} for the renderer at the specified\n   *     index.\n   */\n  public boolean isEquivalent(@Nullable TrackSelectorResult other, int index) {\n    if (other == null) {\n      return false;\n    }\n    return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index])\n        && Util.areEqual(selections.get(index), other.selections.get(index));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.trackselection;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\n/**\n * An allocation within a byte array.\n * <p>\n * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()}\n * on the {@link Allocator} from which it was obtained.\n */\npublic final class Allocation {\n\n  /**\n   * The array containing the allocated space. The allocated space might not be at the start of the\n   * array, and so {@link #offset} must be used when indexing into it.\n   */\n  public final byte[] data;\n\n  /**\n   * The offset of the allocated space in {@link #data}.\n   */\n  public final int offset;\n\n  /**\n   * @param data The array containing the allocated space.\n   * @param offset The offset of the allocated space in {@code data}.\n   */\n  public Allocation(byte[] data, int offset) {\n    this.data = data;\n    this.offset = offset;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\n/**\n * A source of allocations.\n */\npublic interface Allocator {\n\n  /**\n   * Obtain an {@link Allocation}.\n   * <p>\n   * When the caller has finished with the {@link Allocation}, it should be returned by calling\n   * {@link #release(Allocation)}.\n   *\n   * @return The {@link Allocation}.\n   */\n  Allocation allocate();\n\n  /**\n   * Releases an {@link Allocation} back to the allocator.\n   *\n   * @param allocation The {@link Allocation} being released.\n   */\n  void release(Allocation allocation);\n\n  /**\n   * Releases an array of {@link Allocation}s back to the allocator.\n   *\n   * @param allocations The array of {@link Allocation}s being released.\n   */\n  void release(Allocation[] allocations);\n\n  /**\n   * Hints to the allocator that it should make a best effort to release any excess\n   * {@link Allocation}s.\n   */\n  void trim();\n\n  /**\n   * Returns the total number of bytes currently allocated.\n   */\n  int getTotalBytesAllocated();\n\n  /**\n   * Returns the length of each individual {@link Allocation}.\n   */\n  int getIndividualAllocationLength();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.content.Context;\nimport android.content.res.AssetManager;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/** A {@link DataSource} for reading from a local asset. */\npublic final class AssetDataSource extends BaseDataSource {\n\n  /**\n   * Thrown when an {@link IOException} is encountered reading a local asset.\n   */\n  public static final class AssetDataSourceException extends IOException {\n\n    public AssetDataSourceException(IOException cause) {\n      super(cause);\n    }\n\n  }\n\n  private final AssetManager assetManager;\n\n  @Nullable private Uri uri;\n  @Nullable private InputStream inputStream;\n  private long bytesRemaining;\n  private boolean opened;\n\n  /** @param context A context. */\n  public AssetDataSource(Context context) {\n    super(/* isNetwork= */ false);\n    this.assetManager = context.getAssets();\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws AssetDataSourceException {\n    try {\n      uri = dataSpec.uri;\n      String path = Assertions.checkNotNull(uri.getPath());\n      if (path.startsWith(\"/android_asset/\")) {\n        path = path.substring(15);\n      } else if (path.startsWith(\"/\")) {\n        path = path.substring(1);\n      }\n      transferInitializing(dataSpec);\n      inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM);\n      long skipped = inputStream.skip(dataSpec.position);\n      if (skipped < dataSpec.position) {\n        // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips\n        // fewer bytes than requested if the skip is beyond the end of the asset's data.\n        throw new EOFException();\n      }\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        bytesRemaining = inputStream.available();\n        if (bytesRemaining == Integer.MAX_VALUE) {\n          // assetManager.open() returns an AssetInputStream, whose available() implementation\n          // returns Integer.MAX_VALUE if the remaining length is greater than (or equal to)\n          // Integer.MAX_VALUE. We don't know the true length in this case, so treat as unbounded.\n          bytesRemaining = C.LENGTH_UNSET;\n        }\n      }\n    } catch (IOException e) {\n      throw new AssetDataSourceException(e);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n    return bytesRemaining;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    int bytesRead;\n    try {\n      int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength\n          : (int) Math.min(bytesRemaining, readLength);\n      bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);\n    } catch (IOException e) {\n      throw new AssetDataSourceException(e);\n    }\n\n    if (bytesRead == -1) {\n      if (bytesRemaining != C.LENGTH_UNSET) {\n        // End of stream reached having not read sufficient data.\n        throw new AssetDataSourceException(new EOFException());\n      }\n      return C.RESULT_END_OF_INPUT;\n    }\n    if (bytesRemaining != C.LENGTH_UNSET) {\n      bytesRemaining -= bytesRead;\n    }\n    bytesTransferred(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public void close() throws AssetDataSourceException {\n    uri = null;\n    try {\n      if (inputStream != null) {\n        inputStream.close();\n      }\n    } catch (IOException e) {\n      throw new AssetDataSourceException(e);\n    } finally {\n      inputStream = null;\n      if (opened) {\n        opened = false;\n        transferEnded();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\n\n/**\n * Provides estimates of the currently available bandwidth.\n */\npublic interface BandwidthMeter {\n\n  /**\n   * A listener of {@link BandwidthMeter} events.\n   */\n  interface EventListener {\n\n    /**\n     * Called periodically to indicate that bytes have been transferred or the estimated bitrate has\n     * changed.\n     *\n     * <p>Note: The estimated bitrate is typically derived from more information than just {@code\n     * bytes} and {@code elapsedMs}.\n     *\n     * @param elapsedMs The time taken to transfer {@code bytesTransferred}, in milliseconds. This\n     *     is at most the elapsed time since the last callback, but may be less if there were\n     *     periods during which data was not being transferred.\n     * @param bytesTransferred The number of bytes transferred since the last callback.\n     * @param bitrateEstimate The estimated bitrate in bits/sec.\n     */\n    void onBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate);\n  }\n\n  /** Returns the estimated bitrate. */\n  long getBitrateEstimate();\n\n  /**\n   * Returns the {@link TransferListener} that this instance uses to gather bandwidth information\n   * from data transfers. May be null if the implementation does not listen to data transfers.\n   */\n  @Nullable\n  TransferListener getTransferListener();\n\n  /**\n   * Adds an {@link EventListener}.\n   *\n   * @param eventHandler A handler for events.\n   * @param eventListener A listener of events.\n   */\n  void addEventListener(Handler eventHandler, EventListener eventListener);\n\n  /**\n   * Removes an {@link EventListener}.\n   *\n   * @param eventListener The listener to be removed.\n   */\n  void removeEventListener(EventListener eventListener);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport androidx.annotation.Nullable;\nimport java.util.ArrayList;\n\n/**\n * Base {@link DataSource} implementation to keep a list of {@link TransferListener}s.\n *\n * <p>Subclasses must call {@link #transferInitializing(DataSpec)}, {@link\n * #transferStarted(DataSpec)}, {@link #bytesTransferred(int)}, and {@link #transferEnded()} to\n * inform listeners of data transfers.\n */\npublic abstract class BaseDataSource implements DataSource {\n\n  private final boolean isNetwork;\n  private final ArrayList<TransferListener> listeners;\n\n  private int listenerCount;\n  @Nullable private DataSpec dataSpec;\n\n  /**\n   * Creates base data source.\n   *\n   * @param isNetwork Whether the data source loads data through a network.\n   */\n  protected BaseDataSource(boolean isNetwork) {\n    this.isNetwork = isNetwork;\n    this.listeners = new ArrayList<>(/* initialCapacity= */ 1);\n  }\n\n  @Override\n  public final void addTransferListener(TransferListener transferListener) {\n    if (!listeners.contains(transferListener)) {\n      listeners.add(transferListener);\n      listenerCount++;\n    }\n  }\n\n  /**\n   * Notifies listeners that data transfer for the specified {@link DataSpec} is being initialized.\n   *\n   * @param dataSpec {@link DataSpec} describing the data for initializing transfer.\n   */\n  protected final void transferInitializing(DataSpec dataSpec) {\n    for (int i = 0; i < listenerCount; i++) {\n      listeners.get(i).onTransferInitializing(/* source= */ this, dataSpec, isNetwork);\n    }\n  }\n\n  /**\n   * Notifies listeners that data transfer for the specified {@link DataSpec} started.\n   *\n   * @param dataSpec {@link DataSpec} describing the data being transferred.\n   */\n  protected final void transferStarted(DataSpec dataSpec) {\n    this.dataSpec = dataSpec;\n    for (int i = 0; i < listenerCount; i++) {\n      listeners.get(i).onTransferStart(/* source= */ this, dataSpec, isNetwork);\n    }\n  }\n\n  /**\n   * Notifies listeners that bytes were transferred.\n   *\n   * @param bytesTransferred The number of bytes transferred since the previous call to this method\n   *     (or if the first call, since the transfer was started).\n   */\n  protected final void bytesTransferred(int bytesTransferred) {\n    DataSpec dataSpec = castNonNull(this.dataSpec);\n    for (int i = 0; i < listenerCount; i++) {\n      listeners\n          .get(i)\n          .onBytesTransferred(/* source= */ this, dataSpec, isNetwork, bytesTransferred);\n    }\n  }\n\n  /** Notifies listeners that a transfer ended. */\n  protected final void transferEnded() {\n    DataSpec dataSpec = castNonNull(this.dataSpec);\n    for (int i = 0; i < listenerCount; i++) {\n      listeners.get(i).onTransferEnd(/* source= */ this, dataSpec, isNetwork);\n    }\n    this.dataSpec = null;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A {@link DataSink} for writing to a byte array.\n */\npublic final class ByteArrayDataSink implements DataSink {\n\n  private @MonotonicNonNull ByteArrayOutputStream stream;\n\n  @Override\n  public void open(DataSpec dataSpec) {\n    if (dataSpec.length == C.LENGTH_UNSET) {\n      stream = new ByteArrayOutputStream();\n    } else {\n      Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);\n      stream = new ByteArrayOutputStream((int) dataSpec.length);\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    castNonNull(stream).close();\n  }\n\n  @Override\n  public void write(byte[] buffer, int offset, int length) {\n    castNonNull(stream).write(buffer, offset, length);\n  }\n\n  /**\n   * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if\n   * {@link #open(DataSpec)} has never been called.\n   */\n  @Nullable\n  public byte[] getData() {\n    return stream == null ? null : stream.toByteArray();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\n\n/** A {@link DataSource} for reading from a byte array. */\npublic final class ByteArrayDataSource extends BaseDataSource {\n\n  private final byte[] data;\n\n  @Nullable private Uri uri;\n  private int readPosition;\n  private int bytesRemaining;\n  private boolean opened;\n\n  /**\n   * @param data The data to be read.\n   */\n  public ByteArrayDataSource(byte[] data) {\n    super(/* isNetwork= */ false);\n    Assertions.checkNotNull(data);\n    Assertions.checkArgument(data.length > 0);\n    this.data = data;\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    uri = dataSpec.uri;\n    transferInitializing(dataSpec);\n    readPosition = (int) dataSpec.position;\n    bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET)\n        ? (data.length - dataSpec.position) : dataSpec.length);\n    if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {\n      throw new IOException(\"Unsatisfiable range: [\" + readPosition + \", \" + dataSpec.length\n          + \"], length: \" + data.length);\n    }\n    opened = true;\n    transferStarted(dataSpec);\n    return bytesRemaining;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) {\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    readLength = Math.min(readLength, bytesRemaining);\n    System.arraycopy(data, readPosition, buffer, offset, readLength);\n    readPosition += readLength;\n    bytesRemaining -= readLength;\n    bytesTransferred(readLength);\n    return readLength;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public void close() {\n    if (opened) {\n      opened = false;\n      transferEnded();\n    }\n    uri = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.res.AssetFileDescriptor;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.EOFException;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.nio.channels.FileChannel;\n\n/** A {@link DataSource} for reading from a content URI. */\npublic final class ContentDataSource extends BaseDataSource {\n\n  /**\n   * Thrown when an {@link IOException} is encountered reading from a content URI.\n   */\n  public static class ContentDataSourceException extends IOException {\n\n    public ContentDataSourceException(IOException cause) {\n      super(cause);\n    }\n\n  }\n\n  private final ContentResolver resolver;\n\n  @Nullable private Uri uri;\n  @Nullable private AssetFileDescriptor assetFileDescriptor;\n  @Nullable private FileInputStream inputStream;\n  private long bytesRemaining;\n  private boolean opened;\n\n  /**\n   * @param context A context.\n   */\n  public ContentDataSource(Context context) {\n    super(/* isNetwork= */ false);\n    this.resolver = context.getContentResolver();\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws ContentDataSourceException {\n    try {\n      Uri uri = dataSpec.uri;\n      this.uri = uri;\n\n      transferInitializing(dataSpec);\n      AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, \"r\");\n      this.assetFileDescriptor = assetFileDescriptor;\n      if (assetFileDescriptor == null) {\n        throw new FileNotFoundException(\"Could not open file descriptor for: \" + uri);\n      }\n      FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());\n      this.inputStream = inputStream;\n\n      long assetStartOffset = assetFileDescriptor.getStartOffset();\n      long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset;\n      if (skipped != dataSpec.position) {\n        // We expect the skip to be satisfied in full. If it isn't then we're probably trying to\n        // skip beyond the end of the data.\n        throw new EOFException();\n      }\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        long assetFileDescriptorLength = assetFileDescriptor.getLength();\n        if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) {\n          // The asset must extend to the end of the file. If FileInputStream.getChannel().size()\n          // returns 0 then the remaining length cannot be determined.\n          FileChannel channel = inputStream.getChannel();\n          long channelSize = channel.size();\n          bytesRemaining = channelSize == 0 ? C.LENGTH_UNSET : channelSize - channel.position();\n        } else {\n          bytesRemaining = assetFileDescriptorLength - skipped;\n        }\n      }\n    } catch (IOException e) {\n      throw new ContentDataSourceException(e);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n\n    return bytesRemaining;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException {\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    int bytesRead;\n    try {\n      int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength\n          : (int) Math.min(bytesRemaining, readLength);\n      bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);\n    } catch (IOException e) {\n      throw new ContentDataSourceException(e);\n    }\n\n    if (bytesRead == -1) {\n      if (bytesRemaining != C.LENGTH_UNSET) {\n        // End of stream reached having not read sufficient data.\n        throw new ContentDataSourceException(new EOFException());\n      }\n      return C.RESULT_END_OF_INPUT;\n    }\n    if (bytesRemaining != C.LENGTH_UNSET) {\n      bytesRemaining -= bytesRead;\n    }\n    bytesTransferred(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @SuppressWarnings(\"Finally\")\n  @Override\n  public void close() throws ContentDataSourceException {\n    uri = null;\n    try {\n      if (inputStream != null) {\n        inputStream.close();\n      }\n    } catch (IOException e) {\n      throw new ContentDataSourceException(e);\n    } finally {\n      inputStream = null;\n      try {\n        if (assetFileDescriptor != null) {\n          assetFileDescriptor.close();\n        }\n      } catch (IOException e) {\n        throw new ContentDataSourceException(e);\n      } finally {\n        assetFileDescriptor = null;\n        if (opened) {\n          opened = false;\n          transferEnded();\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.net.Uri;\nimport android.util.Base64;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.net.URLDecoder;\n\n/** A {@link DataSource} for reading data URLs, as defined by RFC 2397. */\npublic final class DataSchemeDataSource extends BaseDataSource {\n\n  public static final String SCHEME_DATA = \"data\";\n\n  @Nullable private DataSpec dataSpec;\n  @Nullable private byte[] data;\n  private int endPosition;\n  private int readPosition;\n\n  // the constructor does not initialize fields: data\n  @SuppressWarnings(\"nullness:initialization.fields.uninitialized\")\n  public DataSchemeDataSource() {\n    super(/* isNetwork= */ false);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    transferInitializing(dataSpec);\n    this.dataSpec = dataSpec;\n    readPosition = (int) dataSpec.position;\n    Uri uri = dataSpec.uri;\n    String scheme = uri.getScheme();\n    if (!SCHEME_DATA.equals(scheme)) {\n      throw new ParserException(\"Unsupported scheme: \" + scheme);\n    }\n    String[] uriParts = Util.split(uri.getSchemeSpecificPart(), \",\");\n    if (uriParts.length != 2) {\n      throw new ParserException(\"Unexpected URI format: \" + uri);\n    }\n    String dataString = uriParts[1];\n    if (uriParts[0].contains(\";base64\")) {\n      try {\n        data = Base64.decode(dataString, 0);\n      } catch (IllegalArgumentException e) {\n        throw new ParserException(\"Error while parsing Base64 encoded string: \" + dataString, e);\n      }\n    } else {\n      // TODO: Add support for other charsets.\n      data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME));\n    }\n    endPosition =\n        dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;\n    if (endPosition > data.length || readPosition > endPosition) {\n      data = null;\n      throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);\n    }\n    transferStarted(dataSpec);\n    return (long) endPosition - readPosition;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) {\n    if (readLength == 0) {\n      return 0;\n    }\n    int remainingBytes = endPosition - readPosition;\n    if (remainingBytes == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    readLength = Math.min(readLength, remainingBytes);\n    System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength);\n    readPosition += readLength;\n    bytesTransferred(readLength);\n    return readLength;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return dataSpec != null ? dataSpec.uri : null;\n  }\n\n  @Override\n  public void close() {\n    if (data != null) {\n      data = null;\n      transferEnded();\n    }\n    dataSpec = null;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport java.io.IOException;\n\n/**\n * A component to which streams of data can be written.\n */\npublic interface DataSink {\n\n  /**\n   * A factory for {@link DataSink} instances.\n   */\n  interface Factory {\n\n    /**\n     * Creates a {@link DataSink} instance.\n     */\n    DataSink createDataSink();\n\n  }\n\n  /**\n   * Opens the sink to consume the specified data.\n   *\n   * <p>Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to\n   * ensure that any partial effects of the invocation are cleaned up.\n   *\n   * @param dataSpec Defines the data to be consumed.\n   * @throws IOException If an error occurs opening the sink.\n   */\n  void open(DataSpec dataSpec) throws IOException;\n\n  /**\n   * Consumes the provided data.\n   *\n   * @param buffer The buffer from which data should be consumed.\n   * @param offset The offset of the data to consume in {@code buffer}.\n   * @param length The length of the data to consume, in bytes.\n   * @throws IOException If an error occurs writing to the sink.\n   */\n  void write(byte[] buffer, int offset, int length) throws IOException;\n\n  /**\n   * Closes the sink.\n   *\n   * <p>Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}\n   * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.\n   *\n   * @throws IOException If an error occurs closing the sink.\n   */\n  void close() throws IOException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A component from which streams of data can be read.\n */\npublic interface DataSource {\n\n  /**\n   * A factory for {@link DataSource} instances.\n   */\n  interface Factory {\n\n    /**\n     * Creates a {@link DataSource} instance.\n     */\n    DataSource createDataSource();\n  }\n\n  /**\n   * Adds a {@link TransferListener} to listen to data transfers. This method is not thread-safe.\n   *\n   * @param transferListener A {@link TransferListener}.\n   */\n  void addTransferListener(TransferListener transferListener);\n\n  /**\n   * Opens the source to read the specified data.\n   * <p>\n   * Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to ensure\n   * that any partial effects of the invocation are cleaned up.\n   *\n   * @param dataSpec Defines the data to be read.\n   * @throws IOException If an error occurs opening the source. {@link DataSourceException} can be\n   *     thrown or used as a cause of the thrown exception to specify the reason of the error.\n   * @return The number of bytes that can be read from the opened source. For unbounded requests\n   *     (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value\n   *     is the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still\n   *     unresolved. For all other requests, the value returned will be equal to the request's\n   *     {@link DataSpec#length}.\n   */\n  long open(DataSpec dataSpec) throws IOException;\n\n  /**\n   * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at\n   * index {@code offset}.\n   *\n   * <p>If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because\n   * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.\n   * Otherwise, the call will block until at least one byte of data has been read and the number of\n   * bytes read is returned.\n   *\n   * @param buffer The buffer into which the read data should be stored.\n   * @param offset The start offset into {@code buffer} at which data should be written.\n   * @param readLength The maximum number of bytes to read.\n   * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available\n   *     because the end of the opened range has been reached.\n   * @throws IOException If an error occurs reading from the source.\n   */\n  int read(byte[] buffer, int offset, int readLength) throws IOException;\n\n  /**\n   * When the source is open, returns the {@link Uri} from which data is being read. The returned\n   * {@link Uri} will be identical to the one passed {@link #open(DataSpec)} in the {@link DataSpec}\n   * unless redirection has occurred. If redirection has occurred, the {@link Uri} after redirection\n   * is returned.\n   *\n   * @return The {@link Uri} from which data is being read, or null if the source is not open.\n   */\n  @Nullable Uri getUri();\n\n  /**\n   * When the source is open, returns the response headers associated with the last {@link #open}\n   * call. Otherwise, returns an empty map.\n   */\n  default Map<String, List<String>> getResponseHeaders() {\n    return Collections.emptyMap();\n  }\n\n  /**\n   * Closes the source.\n   * <p>\n   * Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}\n   * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.\n   *\n   * @throws IOException If an error occurs closing the source.\n   */\n  void close() throws IOException;\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport java.io.IOException;\n\n/**\n * Used to specify reason of a DataSource error.\n */\npublic final class DataSourceException extends IOException {\n\n  public static final int POSITION_OUT_OF_RANGE = 0;\n\n  /**\n   * The reason of this {@link DataSourceException}. It can only be {@link #POSITION_OUT_OF_RANGE}.\n   */\n  public final int reason;\n\n  /**\n   * Constructs a DataSourceException.\n   *\n   * @param reason Reason of the error. It can only be {@link #POSITION_OUT_OF_RANGE}.\n   */\n  public DataSourceException(int reason) {\n    this.reason = reason;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport androidx.annotation.NonNull;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and\n * consumed through an {@link InputStream}.\n */\npublic final class DataSourceInputStream extends InputStream {\n\n  private final DataSource dataSource;\n  private final DataSpec dataSpec;\n  private final byte[] singleByteArray;\n\n  private boolean opened = false;\n  private boolean closed = false;\n  private long totalBytesRead;\n\n  /**\n   * @param dataSource The {@link DataSource} from which the data should be read.\n   * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}.\n   */\n  public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {\n    this.dataSource = dataSource;\n    this.dataSpec = dataSpec;\n    singleByteArray = new byte[1];\n  }\n\n  /**\n   * Returns the total number of bytes that have been read or skipped.\n   */\n  public long bytesRead() {\n    return totalBytesRead;\n  }\n\n  /**\n   * Optional call to open the underlying {@link DataSource}.\n   * <p>\n   * Calling this method does nothing if the {@link DataSource} is already open. Calling this\n   * method is optional, since the read and skip methods will automatically open the underlying\n   * {@link DataSource} if it's not open already.\n   *\n   * @throws IOException If an error occurs opening the {@link DataSource}.\n   */\n  public void open() throws IOException {\n    checkOpened();\n  }\n\n  @Override\n  public int read() throws IOException {\n    int length = read(singleByteArray);\n    return length == -1 ? -1 : (singleByteArray[0] & 0xFF);\n  }\n\n  @Override\n  public int read(@NonNull byte[] buffer) throws IOException {\n    return read(buffer, 0, buffer.length);\n  }\n\n  @Override\n  public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {\n    Assertions.checkState(!closed);\n    checkOpened();\n    int bytesRead = dataSource.read(buffer, offset, length);\n    if (bytesRead == C.RESULT_END_OF_INPUT) {\n      return -1;\n    } else {\n      totalBytesRead += bytesRead;\n      return bytesRead;\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (!closed) {\n      dataSource.close();\n      closed = true;\n    }\n  }\n\n  private void checkOpened() throws IOException {\n    if (!opened) {\n      dataSource.open(dataSpec);\n      opened = true;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Defines a region of data.\n */\npublic final class DataSpec {\n\n  /**\n   * The flags that apply to any request for data. Possible flag values are {@link\n   * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link\n   * #FLAG_ALLOW_CACHE_FRAGMENTATION}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION})\n  public @interface Flags {}\n  /**\n   * Allows an underlying network stack to request that the server use gzip compression.\n   *\n   * <p>Should not typically be set if the data being requested is already compressed (e.g. most\n   * audio and video requests). May be set when requesting other data.\n   *\n   * <p>When a {@link DataSource} is used to request data with this flag set, and if the {@link\n   * DataSource} does make a network request, then the value returned from {@link\n   * DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from {@link\n   * DataSource#read(byte[], int, int)} will be the decompressed data.\n   */\n  public static final int FLAG_ALLOW_GZIP = 1;\n  /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */\n  public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2\n  /**\n   * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy\n   * will be able to evict individual fragments of the data. Depending on the cache implementation,\n   * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment\n   * whilst writing another).\n   */\n  public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4\n\n  /**\n   * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link\n   * #HTTP_METHOD_GET}, {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD})\n  public @interface HttpMethod {}\n\n  public static final int HTTP_METHOD_GET = 1;\n  public static final int HTTP_METHOD_POST = 2;\n  public static final int HTTP_METHOD_HEAD = 3;\n\n  /**\n   * The source from which data should be read.\n   */\n  public final Uri uri;\n\n  /**\n   * The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec.\n   * This value will be ignored by non-http {@link DataSource}s.\n   */\n  public final @HttpMethod int httpMethod;\n\n  /**\n   * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be\n   * non-zero.\n   */\n  @Nullable public final byte[] httpBody;\n\n  /** Immutable map containing the headers to use in HTTP requests. */\n  public final Map<String, String> httpRequestHeaders;\n\n  /** The absolute position of the data in the full stream. */\n  public final long absoluteStreamPosition;\n  /**\n   * The position of the data when read from {@link #uri}.\n   * <p>\n   * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location\n   * of a subset of the underlying data.\n   */\n  public final long position;\n  /**\n   * The length of the data, or {@link C#LENGTH_UNSET}.\n   */\n  public final long length;\n  /**\n   * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the\n   * data spec is not intended to be used in conjunction with a cache.\n   */\n  @Nullable public final String key;\n  /** Request {@link Flags flags}. */\n  public final @Flags int flags;\n\n  /**\n   * Construct a data spec for the given uri and with {@link #key} set to null.\n   *\n   * @param uri {@link #uri}.\n   */\n  public DataSpec(Uri uri) {\n    this(uri, 0);\n  }\n\n  /**\n   * Construct a data spec for the given uri and with {@link #key} set to null.\n   *\n   * @param uri {@link #uri}.\n   * @param flags {@link #flags}.\n   */\n  public DataSpec(Uri uri, @Flags int flags) {\n    this(uri, 0, C.LENGTH_UNSET, null, flags);\n  }\n\n  /**\n   * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}.\n   *\n   * @param uri {@link #uri}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   */\n  public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) {\n    this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);\n  }\n\n  /**\n   * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}.\n   *\n   * @param uri {@link #uri}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   */\n  public DataSpec(\n      Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {\n    this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);\n  }\n\n  /**\n   * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has\n   * request headers.\n   *\n   * @param uri {@link #uri}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   * @param httpRequestHeaders {@link #httpRequestHeaders}\n   */\n  public DataSpec(\n      Uri uri,\n      long absoluteStreamPosition,\n      long length,\n      @Nullable String key,\n      @Flags int flags,\n      Map<String, String> httpRequestHeaders) {\n    this(\n        uri,\n        inferHttpMethod(null),\n        null,\n        absoluteStreamPosition,\n        absoluteStreamPosition,\n        length,\n        key,\n        flags,\n        httpRequestHeaders);\n  }\n\n  /**\n   * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}.\n   *\n   * @param uri {@link #uri}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}.\n   * @param position {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   */\n  public DataSpec(\n      Uri uri,\n      long absoluteStreamPosition,\n      long position,\n      long length,\n      @Nullable String key,\n      @Flags int flags) {\n    this(uri, null, absoluteStreamPosition, position, length, key, flags);\n  }\n\n  /**\n   * Construct a data spec by inferring the {@link #httpMethod} based on the {@code postBody}\n   * parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If\n   * postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}.\n   *\n   * @param uri {@link #uri}.\n   * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the\n   *     {@link #httpMethod}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}.\n   * @param position {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   */\n  public DataSpec(\n      Uri uri,\n      @Nullable byte[] postBody,\n      long absoluteStreamPosition,\n      long position,\n      long length,\n      @Nullable String key,\n      @Flags int flags) {\n    this(\n        uri,\n        /* httpMethod= */ inferHttpMethod(postBody),\n        /* httpBody= */ postBody,\n        absoluteStreamPosition,\n        position,\n        length,\n        key,\n        flags);\n  }\n\n  /**\n   * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}.\n   *\n   * @param uri {@link #uri}.\n   * @param httpMethod {@link #httpMethod}.\n   * @param httpBody {@link #httpBody}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}.\n   * @param position {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   */\n  public DataSpec(\n      Uri uri,\n      @HttpMethod int httpMethod,\n      @Nullable byte[] httpBody,\n      long absoluteStreamPosition,\n      long position,\n      long length,\n      @Nullable String key,\n      @Flags int flags) {\n    this(\n        uri,\n        httpMethod,\n        httpBody,\n        absoluteStreamPosition,\n        position,\n        length,\n        key,\n        flags,\n        /* httpRequestHeaders= */ Collections.emptyMap());\n  }\n\n  /**\n   * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests.\n   *\n   * @param uri {@link #uri}.\n   * @param httpMethod {@link #httpMethod}.\n   * @param httpBody {@link #httpBody}.\n   * @param absoluteStreamPosition {@link #absoluteStreamPosition}.\n   * @param position {@link #position}.\n   * @param length {@link #length}.\n   * @param key {@link #key}.\n   * @param flags {@link #flags}.\n   * @param httpRequestHeaders {@link #httpRequestHeaders}.\n   */\n  public DataSpec(\n      Uri uri,\n      @HttpMethod int httpMethod,\n      @Nullable byte[] httpBody,\n      long absoluteStreamPosition,\n      long position,\n      long length,\n      @Nullable String key,\n      @Flags int flags,\n      Map<String, String> httpRequestHeaders) {\n    Assertions.checkArgument(absoluteStreamPosition >= 0);\n    Assertions.checkArgument(position >= 0);\n    Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);\n    this.uri = uri;\n    this.httpMethod = httpMethod;\n    this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null;\n    this.absoluteStreamPosition = absoluteStreamPosition;\n    this.position = position;\n    this.length = length;\n    this.key = key;\n    this.flags = flags;\n    this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders));\n  }\n\n  /**\n   * Returns whether the given flag is set.\n   *\n   * @param flag Flag to be checked if it is set.\n   */\n  public boolean isFlagSet(@Flags int flag) {\n    return (this.flags & flag) == flag;\n  }\n\n  @Override\n  public String toString() {\n    return \"DataSpec[\"\n        + getHttpMethodString()\n        + \" \"\n        + uri\n        + \", \"\n        + Arrays.toString(httpBody)\n        + \", \"\n        + absoluteStreamPosition\n        + \", \"\n        + position\n        + \", \"\n        + length\n        + \", \"\n        + key\n        + \", \"\n        + flags\n        + \"]\";\n  }\n\n  /**\n   * Returns an uppercase HTTP method name (e.g., \"GET\", \"POST\", \"HEAD\") corresponding to the {@link\n   * #httpMethod}.\n   */\n  public final String getHttpMethodString() {\n    return getStringForHttpMethod(httpMethod);\n  }\n\n  /**\n   * Returns an uppercase HTTP method name (e.g., \"GET\", \"POST\", \"HEAD\") corresponding to the {@code\n   * httpMethod}.\n   */\n  public static String getStringForHttpMethod(@HttpMethod int httpMethod) {\n    switch (httpMethod) {\n      case HTTP_METHOD_GET:\n        return \"GET\";\n      case HTTP_METHOD_POST:\n        return \"POST\";\n      case HTTP_METHOD_HEAD:\n        return \"HEAD\";\n      default:\n        throw new AssertionError(httpMethod);\n    }\n  }\n\n  /**\n   * Returns a data spec that represents a subrange of the data defined by this DataSpec. The\n   * subrange includes data from the offset up to the end of this DataSpec.\n   *\n   * @param offset The offset of the subrange.\n   * @return A data spec that represents a subrange of the data defined by this DataSpec.\n   */\n  public DataSpec subrange(long offset) {\n    return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset);\n  }\n\n  /**\n   * Returns a data spec that represents a subrange of the data defined by this DataSpec.\n   *\n   * @param offset The offset of the subrange.\n   * @param length The length of the subrange.\n   * @return A data spec that represents a subrange of the data defined by this DataSpec.\n   */\n  public DataSpec subrange(long offset, long length) {\n    if (offset == 0 && this.length == length) {\n      return this;\n    } else {\n      return new DataSpec(\n          uri,\n          httpMethod,\n          httpBody,\n          absoluteStreamPosition + offset,\n          position + offset,\n          length,\n          key,\n          flags,\n          httpRequestHeaders);\n    }\n  }\n\n  /**\n   * Returns a copy of this data spec with the specified Uri.\n   *\n   * @param uri The new source {@link Uri}.\n   * @return The copied data spec with the specified Uri.\n   */\n  public DataSpec withUri(Uri uri) {\n    return new DataSpec(\n        uri,\n        httpMethod,\n        httpBody,\n        absoluteStreamPosition,\n        position,\n        length,\n        key,\n        flags,\n        httpRequestHeaders);\n  }\n\n  /**\n   * Returns a copy of this data spec with the specified request headers.\n   *\n   * @param requestHeaders The HTTP request headers.\n   * @return The copied data spec with the specified request headers.\n   */\n  public DataSpec withRequestHeaders(Map<String, String> requestHeaders) {\n    return new DataSpec(\n        uri,\n        httpMethod,\n        httpBody,\n        absoluteStreamPosition,\n        position,\n        length,\n        key,\n        flags,\n        requestHeaders);\n  }\n\n  /**\n   * Returns a copy this data spec with additional request headers.\n   *\n   * <p>Note: Values in {@code requestHeaders} will overwrite values with the same header key that\n   * were previously set in this instance's {@code #httpRequestHeaders}.\n   *\n   * @param requestHeaders The additional HTTP request headers.\n   * @return The copied data with the additional HTTP request headers.\n   */\n  public DataSpec withAdditionalHeaders(Map<String, String> requestHeaders) {\n    Map<String, String> totalHeaders = new HashMap<>(this.httpRequestHeaders);\n    totalHeaders.putAll(requestHeaders);\n\n    return new DataSpec(\n        uri,\n        httpMethod,\n        httpBody,\n        absoluteStreamPosition,\n        position,\n        length,\n        key,\n        flags,\n        totalHeaders);\n  }\n\n  @HttpMethod\n  private static int inferHttpMethod(@Nullable byte[] postBody) {\n    return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Default implementation of {@link Allocator}.\n */\npublic final class DefaultAllocator implements Allocator {\n\n  private static final int AVAILABLE_EXTRA_CAPACITY = 100;\n\n  private final boolean trimOnReset;\n  private final int individualAllocationSize;\n  private final byte[] initialAllocationBlock;\n  private final Allocation[] singleAllocationReleaseHolder;\n\n  private int targetBufferSize;\n  private int allocatedCount;\n  private int availableCount;\n  private Allocation[] availableAllocations;\n\n  /**\n   * Constructs an instance without creating any {@link Allocation}s up front.\n   *\n   * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless\n   *     the allocator will be re-used by multiple player instances.\n   * @param individualAllocationSize The length of each individual {@link Allocation}.\n   */\n  public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {\n    this(trimOnReset, individualAllocationSize, 0);\n  }\n\n  /**\n   * Constructs an instance with some {@link Allocation}s created up front.\n   * <p>\n   * Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.\n   *\n   * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless\n   *     the allocator will be re-used by multiple player instances.\n   * @param individualAllocationSize The length of each individual {@link Allocation}.\n   * @param initialAllocationCount The number of allocations to create up front.\n   */\n  public DefaultAllocator(boolean trimOnReset, int individualAllocationSize,\n      int initialAllocationCount) {\n    Assertions.checkArgument(individualAllocationSize > 0);\n    Assertions.checkArgument(initialAllocationCount >= 0);\n    this.trimOnReset = trimOnReset;\n    this.individualAllocationSize = individualAllocationSize;\n    this.availableCount = initialAllocationCount;\n    this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];\n    if (initialAllocationCount > 0) {\n      initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize];\n      for (int i = 0; i < initialAllocationCount; i++) {\n        int allocationOffset = i * individualAllocationSize;\n        availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset);\n      }\n    } else {\n      initialAllocationBlock = null;\n    }\n    singleAllocationReleaseHolder = new Allocation[1];\n  }\n\n  public synchronized void reset() {\n    if (trimOnReset) {\n      setTargetBufferSize(0);\n    }\n  }\n\n  public synchronized void setTargetBufferSize(int targetBufferSize) {\n    boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize;\n    this.targetBufferSize = targetBufferSize;\n    if (targetBufferSizeReduced) {\n      trim();\n    }\n  }\n\n  @Override\n  public synchronized Allocation allocate() {\n    allocatedCount++;\n    Allocation allocation;\n    if (availableCount > 0) {\n      allocation = availableAllocations[--availableCount];\n      availableAllocations[availableCount] = null;\n    } else {\n      allocation = new Allocation(new byte[individualAllocationSize], 0);\n    }\n    return allocation;\n  }\n\n  @Override\n  public synchronized void release(Allocation allocation) {\n    singleAllocationReleaseHolder[0] = allocation;\n    release(singleAllocationReleaseHolder);\n  }\n\n  @Override\n  public synchronized void release(Allocation[] allocations) {\n    if (availableCount + allocations.length >= availableAllocations.length) {\n      availableAllocations = Arrays.copyOf(availableAllocations,\n          Math.max(availableAllocations.length * 2, availableCount + allocations.length));\n    }\n    for (Allocation allocation : allocations) {\n      availableAllocations[availableCount++] = allocation;\n    }\n    allocatedCount -= allocations.length;\n    // Wake up threads waiting for the allocated size to drop.\n    notifyAll();\n  }\n\n  @Override\n  public synchronized void trim() {\n    int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize);\n    int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount);\n    if (targetAvailableCount >= availableCount) {\n      // We're already at or below the target.\n      return;\n    }\n\n    if (initialAllocationBlock != null) {\n      // Some allocations are backed by an initial block. We need to make sure that we hold onto all\n      // such allocations. Re-order the available allocations so that the ones backed by the initial\n      // block come first.\n      int lowIndex = 0;\n      int highIndex = availableCount - 1;\n      while (lowIndex <= highIndex) {\n        Allocation lowAllocation = availableAllocations[lowIndex];\n        if (lowAllocation.data == initialAllocationBlock) {\n          lowIndex++;\n        } else {\n          Allocation highAllocation = availableAllocations[highIndex];\n          if (highAllocation.data != initialAllocationBlock) {\n            highIndex--;\n          } else {\n            availableAllocations[lowIndex++] = highAllocation;\n            availableAllocations[highIndex--] = lowAllocation;\n          }\n        }\n      }\n      // lowIndex is the index of the first allocation not backed by an initial block.\n      targetAvailableCount = Math.max(targetAvailableCount, lowIndex);\n      if (targetAvailableCount >= availableCount) {\n        // We're already at or below the target.\n        return;\n      }\n    }\n\n    // Discard allocations beyond the target.\n    Arrays.fill(availableAllocations, targetAvailableCount, availableCount, null);\n    availableCount = targetAvailableCount;\n  }\n\n  @Override\n  public synchronized int getTotalBytesAllocated() {\n    return allocatedCount * individualAllocationSize;\n  }\n\n  @Override\n  public int getIndividualAllocationLength() {\n    return individualAllocationSize;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.net.ConnectivityManager;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.SparseArray;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.EventDispatcher;\nimport com.google.android.exoplayer2.util.SlidingPercentile;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * Estimates bandwidth by listening to data transfers.\n *\n * <p>The bandwidth estimate is calculated using a {@link SlidingPercentile} and is updated each\n * time a transfer ends. The initial estimate is based on the current operator's network country\n * code or the locale of the user, as well as the network connection type. This can be configured in\n * the {@link Builder}.\n */\npublic final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {\n\n  /**\n   * Country groups used to determine the default initial bitrate estimate. The group assignment for\n   * each country is an array of group indices for [Wifi, 2G, 3G, 4G].\n   */\n  public static final Map<String, int[]> DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS =\n      createInitialBitrateCountryGroupAssignment();\n\n  /** Default initial Wifi bitrate estimate in bits per second. */\n  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI =\n      new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000};\n\n  /** Default initial 2G bitrate estimates in bits per second. */\n  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G =\n      new long[] {200_000, 148_000, 132_000, 115_000, 95_000};\n\n  /** Default initial 3G bitrate estimates in bits per second. */\n  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G =\n      new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000};\n\n  /** Default initial 4G bitrate estimates in bits per second. */\n  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G =\n      new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000};\n\n  /**\n   * Default initial bitrate estimate used when the device is offline or the network type cannot be\n   * determined, in bits per second.\n   */\n  public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000;\n\n  /** Default maximum weight for the sliding window. */\n  public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000;\n\n  @Nullable private static DefaultBandwidthMeter singletonInstance;\n\n  /** Builder for a bandwidth meter. */\n  public static final class Builder {\n\n    @Nullable private final Context context;\n\n    private SparseArray<Long> initialBitrateEstimates;\n    private int slidingWindowMaxWeight;\n    private Clock clock;\n    private boolean resetOnNetworkTypeChange;\n\n    /**\n     * Creates a builder with default parameters and without listener.\n     *\n     * @param context A context.\n     */\n    public Builder(Context context) {\n      // Handling of null is for backward compatibility only.\n      this.context = context == null ? null : context.getApplicationContext();\n      initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context));\n      slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT;\n      clock = Clock.DEFAULT;\n      resetOnNetworkTypeChange = true;\n    }\n\n    /**\n     * Sets the maximum weight for the sliding window.\n     *\n     * @param slidingWindowMaxWeight The maximum weight for the sliding window.\n     * @return This builder.\n     */\n    public Builder setSlidingWindowMaxWeight(int slidingWindowMaxWeight) {\n      this.slidingWindowMaxWeight = slidingWindowMaxWeight;\n      return this;\n    }\n\n    /**\n     * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth\n     * estimate is unavailable.\n     *\n     * @param initialBitrateEstimate The initial bitrate estimate in bits per second.\n     * @return This builder.\n     */\n    public Builder setInitialBitrateEstimate(long initialBitrateEstimate) {\n      for (int i = 0; i < initialBitrateEstimates.size(); i++) {\n        initialBitrateEstimates.setValueAt(i, initialBitrateEstimate);\n      }\n      return this;\n    }\n\n    /**\n     * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth\n     * estimate is unavailable and the current network connection is of the specified type.\n     *\n     * @param networkType The {@link C.NetworkType} this initial estimate is for.\n     * @param initialBitrateEstimate The initial bitrate estimate in bits per second.\n     * @return This builder.\n     */\n    public Builder setInitialBitrateEstimate(\n        @C.NetworkType int networkType, long initialBitrateEstimate) {\n      initialBitrateEstimates.put(networkType, initialBitrateEstimate);\n      return this;\n    }\n\n    /**\n     * Sets the initial bitrate estimates to the default values of the specified country. The\n     * initial estimates are used when a bandwidth estimate is unavailable.\n     *\n     * @param countryCode The ISO 3166-1 alpha-2 country code of the country whose default bitrate\n     *     estimates should be used.\n     * @return This builder.\n     */\n    public Builder setInitialBitrateEstimate(String countryCode) {\n      initialBitrateEstimates =\n          getInitialBitrateEstimatesForCountry(Util.toUpperInvariant(countryCode));\n      return this;\n    }\n\n    /**\n     * Sets the clock used to estimate bandwidth from data transfers. Should only be set for testing\n     * purposes.\n     *\n     * @param clock The clock used to estimate bandwidth from data transfers.\n     * @return This builder.\n     */\n    public Builder setClock(Clock clock) {\n      this.clock = clock;\n      return this;\n    }\n\n    /**\n     * Sets whether to reset if the network type changes. The default value is {@code true}.\n     *\n     * @param resetOnNetworkTypeChange Whether to reset if the network type changes.\n     * @return This builder.\n     */\n    public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) {\n      this.resetOnNetworkTypeChange = resetOnNetworkTypeChange;\n      return this;\n    }\n\n    /**\n     * Builds the bandwidth meter.\n     *\n     * @return A bandwidth meter with the configured properties.\n     */\n    public DefaultBandwidthMeter build() {\n      return new DefaultBandwidthMeter(\n          context,\n          initialBitrateEstimates,\n          slidingWindowMaxWeight,\n          clock,\n          resetOnNetworkTypeChange);\n    }\n\n    private static SparseArray<Long> getInitialBitrateEstimatesForCountry(String countryCode) {\n      int[] groupIndices = getCountryGroupIndices(countryCode);\n      SparseArray<Long> result = new SparseArray<>(/* initialCapacity= */ 6);\n      result.append(C.NETWORK_TYPE_UNKNOWN, DEFAULT_INITIAL_BITRATE_ESTIMATE);\n      result.append(C.NETWORK_TYPE_WIFI, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);\n      result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]);\n      result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]);\n      result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]);\n      // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate.\n      result.append(\n          C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);\n      return result;\n    }\n\n    private static int[] getCountryGroupIndices(String countryCode) {\n      int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode);\n      // Assume median group if not found.\n      return groupIndices == null ? new int[] {2, 2, 2, 2} : groupIndices;\n    }\n  }\n\n  /**\n   * Returns a singleton instance of a {@link DefaultBandwidthMeter} with default configuration.\n   *\n   * @param context A {@link Context}.\n   * @return The singleton instance.\n   */\n  public static synchronized DefaultBandwidthMeter getSingletonInstance(Context context) {\n    if (singletonInstance == null) {\n      singletonInstance = new Builder(context).build();\n    }\n    return singletonInstance;\n  }\n\n  private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000;\n  private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024;\n\n  @Nullable private final Context context;\n  private final SparseArray<Long> initialBitrateEstimates;\n  private final EventDispatcher<EventListener> eventDispatcher;\n  private final SlidingPercentile slidingPercentile;\n  private final Clock clock;\n\n  private int streamCount;\n  private long sampleStartTimeMs;\n  private long sampleBytesTransferred;\n\n  @C.NetworkType private int networkType;\n  private long totalElapsedTimeMs;\n  private long totalBytesTransferred;\n  private long bitrateEstimate;\n  private long lastReportedBitrateEstimate;\n\n  private boolean networkTypeOverrideSet;\n  @C.NetworkType private int networkTypeOverride;\n\n  /** @deprecated Use {@link Builder} instead. */\n  @Deprecated\n  public DefaultBandwidthMeter() {\n    this(\n        /* context= */ null,\n        /* initialBitrateEstimates= */ new SparseArray<>(),\n        DEFAULT_SLIDING_WINDOW_MAX_WEIGHT,\n        Clock.DEFAULT,\n        /* resetOnNetworkTypeChange= */ false);\n  }\n\n  private DefaultBandwidthMeter(\n      @Nullable Context context,\n      SparseArray<Long> initialBitrateEstimates,\n      int maxWeight,\n      Clock clock,\n      boolean resetOnNetworkTypeChange) {\n    this.context = context == null ? null : context.getApplicationContext();\n    this.initialBitrateEstimates = initialBitrateEstimates;\n    this.eventDispatcher = new EventDispatcher<>();\n    this.slidingPercentile = new SlidingPercentile(maxWeight);\n    this.clock = clock;\n    // Set the initial network type and bitrate estimate\n    networkType = context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context);\n    bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);\n    // Register to receive connectivity actions if possible.\n    if (context != null && resetOnNetworkTypeChange) {\n      ConnectivityActionReceiver connectivityActionReceiver =\n          ConnectivityActionReceiver.getInstance(context);\n      connectivityActionReceiver.register(/* bandwidthMeter= */ this);\n    }\n  }\n\n  /**\n   * Overrides the network type. Handled in the same way as if the meter had detected a change from\n   * the current network type to the specified network type internally.\n   *\n   * <p>Applications should not normally call this method. It is intended for testing purposes.\n   *\n   * @param networkType The overriding network type.\n   */\n  public synchronized void setNetworkTypeOverride(@C.NetworkType int networkType) {\n    networkTypeOverride = networkType;\n    networkTypeOverrideSet = true;\n    onConnectivityAction();\n  }\n\n  @Override\n  public synchronized long getBitrateEstimate() {\n    return bitrateEstimate;\n  }\n\n  @Override\n  @Nullable\n  public TransferListener getTransferListener() {\n    return this;\n  }\n\n  @Override\n  public void addEventListener(Handler eventHandler, EventListener eventListener) {\n    eventDispatcher.addListener(eventHandler, eventListener);\n  }\n\n  @Override\n  public void removeEventListener(EventListener eventListener) {\n    eventDispatcher.removeListener(eventListener);\n  }\n\n  @Override\n  public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {\n    // Do nothing.\n  }\n\n  @Override\n  public synchronized void onTransferStart(\n      DataSource source, DataSpec dataSpec, boolean isNetwork) {\n    if (!isNetwork) {\n      return;\n    }\n    if (streamCount == 0) {\n      sampleStartTimeMs = clock.elapsedRealtime();\n    }\n    streamCount++;\n  }\n\n  @Override\n  public synchronized void onBytesTransferred(\n      DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) {\n    if (!isNetwork) {\n      return;\n    }\n    sampleBytesTransferred += bytes;\n  }\n\n  @Override\n  public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {\n    if (!isNetwork) {\n      return;\n    }\n    Assertions.checkState(streamCount > 0);\n    long nowMs = clock.elapsedRealtime();\n    int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);\n    totalElapsedTimeMs += sampleElapsedTimeMs;\n    totalBytesTransferred += sampleBytesTransferred;\n    if (sampleElapsedTimeMs > 0) {\n      float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs;\n      slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);\n      if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE\n          || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {\n        bitrateEstimate = (long) slidingPercentile.getPercentile(0.5f);\n      }\n      maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);\n      sampleStartTimeMs = nowMs;\n      sampleBytesTransferred = 0;\n    } // Else any sample bytes transferred will be carried forward into the next sample.\n    streamCount--;\n  }\n\n  private synchronized void onConnectivityAction() {\n    int networkType =\n        networkTypeOverrideSet\n            ? networkTypeOverride\n            : (context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context));\n    if (this.networkType == networkType) {\n      return;\n    }\n\n    this.networkType = networkType;\n    if (networkType == C.NETWORK_TYPE_OFFLINE\n        || networkType == C.NETWORK_TYPE_UNKNOWN\n        || networkType == C.NETWORK_TYPE_OTHER) {\n      // It's better not to reset the bandwidth meter for these network types.\n      return;\n    }\n\n    // Reset the bitrate estimate and report it, along with any bytes transferred.\n    this.bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);\n    long nowMs = clock.elapsedRealtime();\n    int sampleElapsedTimeMs = streamCount > 0 ? (int) (nowMs - sampleStartTimeMs) : 0;\n    maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);\n\n    // Reset the remainder of the state.\n    sampleStartTimeMs = nowMs;\n    sampleBytesTransferred = 0;\n    totalBytesTransferred = 0;\n    totalElapsedTimeMs = 0;\n    slidingPercentile.reset();\n  }\n\n  private void maybeNotifyBandwidthSample(\n      int elapsedMs, long bytesTransferred, long bitrateEstimate) {\n    if (elapsedMs == 0 && bytesTransferred == 0 && bitrateEstimate == lastReportedBitrateEstimate) {\n      return;\n    }\n    lastReportedBitrateEstimate = bitrateEstimate;\n    eventDispatcher.dispatch(\n        listener -> listener.onBandwidthSample(elapsedMs, bytesTransferred, bitrateEstimate));\n  }\n\n  private long getInitialBitrateEstimateForNetworkType(@C.NetworkType int networkType) {\n    Long initialBitrateEstimate = initialBitrateEstimates.get(networkType);\n    if (initialBitrateEstimate == null) {\n      initialBitrateEstimate = initialBitrateEstimates.get(C.NETWORK_TYPE_UNKNOWN);\n    }\n    if (initialBitrateEstimate == null) {\n      initialBitrateEstimate = DEFAULT_INITIAL_BITRATE_ESTIMATE;\n    }\n    return initialBitrateEstimate;\n  }\n\n  /*\n   * Note: This class only holds a weak reference to DefaultBandwidthMeter instances. It should not\n   * be made non-static, since doing so adds a strong reference (i.e. DefaultBandwidthMeter.this).\n   */\n  private static class ConnectivityActionReceiver extends BroadcastReceiver {\n\n    private static @MonotonicNonNull ConnectivityActionReceiver staticInstance;\n\n    private final Handler mainHandler;\n    private final ArrayList<WeakReference<DefaultBandwidthMeter>> bandwidthMeters;\n\n    public static synchronized ConnectivityActionReceiver getInstance(Context context) {\n      if (staticInstance == null) {\n        staticInstance = new ConnectivityActionReceiver();\n        IntentFilter filter = new IntentFilter();\n        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);\n        context.registerReceiver(staticInstance, filter);\n      }\n      return staticInstance;\n    }\n\n    private ConnectivityActionReceiver() {\n      mainHandler = new Handler(Looper.getMainLooper());\n      bandwidthMeters = new ArrayList<>();\n    }\n\n    public synchronized void register(DefaultBandwidthMeter bandwidthMeter) {\n      removeClearedReferences();\n      bandwidthMeters.add(new WeakReference<>(bandwidthMeter));\n      // Simulate an initial update on the main thread (like the sticky broadcast we'd receive if\n      // we were to register a separate broadcast receiver for each bandwidth meter).\n      mainHandler.post(() -> updateBandwidthMeter(bandwidthMeter));\n    }\n\n    @Override\n    public synchronized void onReceive(Context context, Intent intent) {\n      if (isInitialStickyBroadcast()) {\n        return;\n      }\n      removeClearedReferences();\n      for (int i = 0; i < bandwidthMeters.size(); i++) {\n        WeakReference<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);\n        DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();\n        if (bandwidthMeter != null) {\n          updateBandwidthMeter(bandwidthMeter);\n        }\n      }\n    }\n\n    private void updateBandwidthMeter(DefaultBandwidthMeter bandwidthMeter) {\n      bandwidthMeter.onConnectivityAction();\n    }\n\n    private void removeClearedReferences() {\n      for (int i = bandwidthMeters.size() - 1; i >= 0; i--) {\n        WeakReference<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);\n        DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();\n        if (bandwidthMeter == null) {\n          bandwidthMeters.remove(i);\n        }\n      }\n    }\n  }\n\n  private static Map<String, int[]> createInitialBitrateCountryGroupAssignment() {\n    HashMap<String, int[]> countryGroupAssignment = new HashMap<>();\n    countryGroupAssignment.put(\"AD\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"AE\", new int[] {1, 4, 4, 4});\n    countryGroupAssignment.put(\"AF\", new int[] {4, 4, 3, 3});\n    countryGroupAssignment.put(\"AG\", new int[] {3, 1, 0, 1});\n    countryGroupAssignment.put(\"AI\", new int[] {1, 0, 0, 3});\n    countryGroupAssignment.put(\"AL\", new int[] {1, 2, 0, 1});\n    countryGroupAssignment.put(\"AM\", new int[] {2, 2, 2, 2});\n    countryGroupAssignment.put(\"AO\", new int[] {3, 4, 2, 0});\n    countryGroupAssignment.put(\"AR\", new int[] {2, 3, 2, 2});\n    countryGroupAssignment.put(\"AS\", new int[] {3, 0, 4, 2});\n    countryGroupAssignment.put(\"AT\", new int[] {0, 3, 0, 0});\n    countryGroupAssignment.put(\"AU\", new int[] {0, 3, 0, 1});\n    countryGroupAssignment.put(\"AW\", new int[] {1, 1, 0, 3});\n    countryGroupAssignment.put(\"AX\", new int[] {0, 3, 0, 2});\n    countryGroupAssignment.put(\"AZ\", new int[] {3, 3, 3, 3});\n    countryGroupAssignment.put(\"BA\", new int[] {1, 1, 0, 1});\n    countryGroupAssignment.put(\"BB\", new int[] {0, 2, 0, 0});\n    countryGroupAssignment.put(\"BD\", new int[] {2, 1, 3, 3});\n    countryGroupAssignment.put(\"BE\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"BF\", new int[] {4, 4, 4, 1});\n    countryGroupAssignment.put(\"BG\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"BH\", new int[] {2, 1, 3, 4});\n    countryGroupAssignment.put(\"BI\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"BJ\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"BL\", new int[] {1, 0, 2, 2});\n    countryGroupAssignment.put(\"BM\", new int[] {1, 2, 0, 0});\n    countryGroupAssignment.put(\"BN\", new int[] {4, 1, 3, 2});\n    countryGroupAssignment.put(\"BO\", new int[] {1, 2, 3, 2});\n    countryGroupAssignment.put(\"BQ\", new int[] {1, 1, 2, 4});\n    countryGroupAssignment.put(\"BR\", new int[] {2, 3, 3, 2});\n    countryGroupAssignment.put(\"BS\", new int[] {2, 1, 1, 4});\n    countryGroupAssignment.put(\"BT\", new int[] {3, 0, 3, 1});\n    countryGroupAssignment.put(\"BW\", new int[] {4, 4, 1, 2});\n    countryGroupAssignment.put(\"BY\", new int[] {0, 1, 1, 2});\n    countryGroupAssignment.put(\"BZ\", new int[] {2, 2, 2, 1});\n    countryGroupAssignment.put(\"CA\", new int[] {0, 3, 1, 3});\n    countryGroupAssignment.put(\"CD\", new int[] {4, 4, 2, 2});\n    countryGroupAssignment.put(\"CF\", new int[] {4, 4, 3, 0});\n    countryGroupAssignment.put(\"CG\", new int[] {3, 4, 2, 4});\n    countryGroupAssignment.put(\"CH\", new int[] {0, 0, 1, 0});\n    countryGroupAssignment.put(\"CI\", new int[] {3, 4, 3, 3});\n    countryGroupAssignment.put(\"CK\", new int[] {2, 4, 1, 0});\n    countryGroupAssignment.put(\"CL\", new int[] {1, 2, 2, 3});\n    countryGroupAssignment.put(\"CM\", new int[] {3, 4, 3, 1});\n    countryGroupAssignment.put(\"CN\", new int[] {2, 0, 2, 3});\n    countryGroupAssignment.put(\"CO\", new int[] {2, 3, 2, 2});\n    countryGroupAssignment.put(\"CR\", new int[] {2, 3, 4, 4});\n    countryGroupAssignment.put(\"CU\", new int[] {4, 4, 3, 1});\n    countryGroupAssignment.put(\"CV\", new int[] {2, 3, 1, 2});\n    countryGroupAssignment.put(\"CW\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"CY\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"CZ\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"DE\", new int[] {0, 1, 1, 3});\n    countryGroupAssignment.put(\"DJ\", new int[] {4, 3, 4, 1});\n    countryGroupAssignment.put(\"DK\", new int[] {0, 0, 1, 1});\n    countryGroupAssignment.put(\"DM\", new int[] {1, 0, 1, 3});\n    countryGroupAssignment.put(\"DO\", new int[] {3, 3, 4, 4});\n    countryGroupAssignment.put(\"DZ\", new int[] {3, 3, 4, 4});\n    countryGroupAssignment.put(\"EC\", new int[] {2, 3, 4, 3});\n    countryGroupAssignment.put(\"EE\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"EG\", new int[] {3, 4, 2, 2});\n    countryGroupAssignment.put(\"EH\", new int[] {2, 0, 3, 3});\n    countryGroupAssignment.put(\"ER\", new int[] {4, 2, 2, 0});\n    countryGroupAssignment.put(\"ES\", new int[] {0, 1, 1, 1});\n    countryGroupAssignment.put(\"ET\", new int[] {4, 4, 4, 0});\n    countryGroupAssignment.put(\"FI\", new int[] {0, 0, 1, 0});\n    countryGroupAssignment.put(\"FJ\", new int[] {3, 0, 3, 3});\n    countryGroupAssignment.put(\"FK\", new int[] {3, 4, 2, 2});\n    countryGroupAssignment.put(\"FM\", new int[] {4, 0, 4, 0});\n    countryGroupAssignment.put(\"FO\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"FR\", new int[] {1, 0, 3, 1});\n    countryGroupAssignment.put(\"GA\", new int[] {3, 3, 2, 2});\n    countryGroupAssignment.put(\"GB\", new int[] {0, 1, 3, 3});\n    countryGroupAssignment.put(\"GD\", new int[] {2, 0, 4, 4});\n    countryGroupAssignment.put(\"GE\", new int[] {1, 1, 1, 4});\n    countryGroupAssignment.put(\"GF\", new int[] {2, 3, 4, 4});\n    countryGroupAssignment.put(\"GG\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"GH\", new int[] {3, 3, 2, 2});\n    countryGroupAssignment.put(\"GI\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"GL\", new int[] {2, 2, 0, 2});\n    countryGroupAssignment.put(\"GM\", new int[] {4, 4, 3, 4});\n    countryGroupAssignment.put(\"GN\", new int[] {3, 4, 4, 2});\n    countryGroupAssignment.put(\"GP\", new int[] {2, 1, 1, 4});\n    countryGroupAssignment.put(\"GQ\", new int[] {4, 4, 3, 0});\n    countryGroupAssignment.put(\"GR\", new int[] {1, 1, 0, 2});\n    countryGroupAssignment.put(\"GT\", new int[] {3, 3, 3, 3});\n    countryGroupAssignment.put(\"GU\", new int[] {1, 2, 4, 4});\n    countryGroupAssignment.put(\"GW\", new int[] {4, 4, 4, 1});\n    countryGroupAssignment.put(\"GY\", new int[] {3, 2, 1, 1});\n    countryGroupAssignment.put(\"HK\", new int[] {0, 2, 3, 4});\n    countryGroupAssignment.put(\"HN\", new int[] {3, 2, 3, 2});\n    countryGroupAssignment.put(\"HR\", new int[] {1, 1, 0, 1});\n    countryGroupAssignment.put(\"HT\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"HU\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"ID\", new int[] {3, 2, 3, 4});\n    countryGroupAssignment.put(\"IE\", new int[] {1, 0, 1, 1});\n    countryGroupAssignment.put(\"IL\", new int[] {0, 0, 2, 3});\n    countryGroupAssignment.put(\"IM\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"IN\", new int[] {2, 2, 4, 4});\n    countryGroupAssignment.put(\"IO\", new int[] {4, 2, 2, 2});\n    countryGroupAssignment.put(\"IQ\", new int[] {3, 3, 4, 2});\n    countryGroupAssignment.put(\"IR\", new int[] {3, 0, 2, 2});\n    countryGroupAssignment.put(\"IS\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"IT\", new int[] {1, 0, 1, 2});\n    countryGroupAssignment.put(\"JE\", new int[] {1, 0, 0, 1});\n    countryGroupAssignment.put(\"JM\", new int[] {2, 3, 3, 1});\n    countryGroupAssignment.put(\"JO\", new int[] {1, 2, 1, 2});\n    countryGroupAssignment.put(\"JP\", new int[] {0, 2, 1, 1});\n    countryGroupAssignment.put(\"KE\", new int[] {3, 4, 4, 3});\n    countryGroupAssignment.put(\"KG\", new int[] {1, 1, 2, 2});\n    countryGroupAssignment.put(\"KH\", new int[] {1, 0, 4, 4});\n    countryGroupAssignment.put(\"KI\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"KM\", new int[] {4, 3, 2, 3});\n    countryGroupAssignment.put(\"KN\", new int[] {1, 0, 1, 3});\n    countryGroupAssignment.put(\"KP\", new int[] {4, 2, 4, 2});\n    countryGroupAssignment.put(\"KR\", new int[] {0, 1, 1, 1});\n    countryGroupAssignment.put(\"KW\", new int[] {2, 3, 1, 1});\n    countryGroupAssignment.put(\"KY\", new int[] {1, 1, 0, 1});\n    countryGroupAssignment.put(\"KZ\", new int[] {1, 2, 2, 3});\n    countryGroupAssignment.put(\"LA\", new int[] {2, 2, 1, 1});\n    countryGroupAssignment.put(\"LB\", new int[] {3, 2, 0, 0});\n    countryGroupAssignment.put(\"LC\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"LI\", new int[] {0, 0, 2, 4});\n    countryGroupAssignment.put(\"LK\", new int[] {2, 1, 2, 3});\n    countryGroupAssignment.put(\"LR\", new int[] {3, 4, 3, 1});\n    countryGroupAssignment.put(\"LS\", new int[] {3, 3, 2, 0});\n    countryGroupAssignment.put(\"LT\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"LU\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"LV\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"LY\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"MA\", new int[] {2, 1, 2, 1});\n    countryGroupAssignment.put(\"MC\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"MD\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"ME\", new int[] {1, 2, 1, 2});\n    countryGroupAssignment.put(\"MF\", new int[] {1, 1, 1, 1});\n    countryGroupAssignment.put(\"MG\", new int[] {3, 4, 2, 2});\n    countryGroupAssignment.put(\"MH\", new int[] {4, 0, 2, 4});\n    countryGroupAssignment.put(\"MK\", new int[] {1, 0, 0, 0});\n    countryGroupAssignment.put(\"ML\", new int[] {4, 4, 2, 0});\n    countryGroupAssignment.put(\"MM\", new int[] {3, 3, 1, 2});\n    countryGroupAssignment.put(\"MN\", new int[] {2, 3, 2, 3});\n    countryGroupAssignment.put(\"MO\", new int[] {0, 0, 4, 4});\n    countryGroupAssignment.put(\"MP\", new int[] {0, 2, 4, 4});\n    countryGroupAssignment.put(\"MQ\", new int[] {2, 1, 1, 4});\n    countryGroupAssignment.put(\"MR\", new int[] {4, 2, 4, 2});\n    countryGroupAssignment.put(\"MS\", new int[] {1, 2, 3, 3});\n    countryGroupAssignment.put(\"MT\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"MU\", new int[] {2, 2, 3, 4});\n    countryGroupAssignment.put(\"MV\", new int[] {4, 3, 0, 2});\n    countryGroupAssignment.put(\"MW\", new int[] {3, 2, 1, 0});\n    countryGroupAssignment.put(\"MX\", new int[] {2, 4, 4, 3});\n    countryGroupAssignment.put(\"MY\", new int[] {2, 2, 3, 3});\n    countryGroupAssignment.put(\"MZ\", new int[] {3, 3, 2, 1});\n    countryGroupAssignment.put(\"NA\", new int[] {3, 3, 2, 1});\n    countryGroupAssignment.put(\"NC\", new int[] {2, 0, 3, 3});\n    countryGroupAssignment.put(\"NE\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"NF\", new int[] {1, 2, 2, 2});\n    countryGroupAssignment.put(\"NG\", new int[] {3, 4, 3, 1});\n    countryGroupAssignment.put(\"NI\", new int[] {3, 3, 4, 4});\n    countryGroupAssignment.put(\"NL\", new int[] {0, 2, 3, 3});\n    countryGroupAssignment.put(\"NO\", new int[] {0, 1, 1, 0});\n    countryGroupAssignment.put(\"NP\", new int[] {2, 2, 2, 2});\n    countryGroupAssignment.put(\"NR\", new int[] {4, 0, 3, 1});\n    countryGroupAssignment.put(\"NZ\", new int[] {0, 0, 1, 2});\n    countryGroupAssignment.put(\"OM\", new int[] {3, 2, 1, 3});\n    countryGroupAssignment.put(\"PA\", new int[] {1, 3, 3, 4});\n    countryGroupAssignment.put(\"PE\", new int[] {2, 3, 4, 4});\n    countryGroupAssignment.put(\"PF\", new int[] {2, 2, 0, 1});\n    countryGroupAssignment.put(\"PG\", new int[] {4, 3, 3, 1});\n    countryGroupAssignment.put(\"PH\", new int[] {3, 0, 3, 4});\n    countryGroupAssignment.put(\"PK\", new int[] {3, 3, 3, 3});\n    countryGroupAssignment.put(\"PL\", new int[] {1, 0, 1, 3});\n    countryGroupAssignment.put(\"PM\", new int[] {0, 2, 2, 0});\n    countryGroupAssignment.put(\"PR\", new int[] {1, 2, 3, 3});\n    countryGroupAssignment.put(\"PS\", new int[] {3, 3, 2, 4});\n    countryGroupAssignment.put(\"PT\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"PW\", new int[] {2, 1, 2, 0});\n    countryGroupAssignment.put(\"PY\", new int[] {2, 0, 2, 3});\n    countryGroupAssignment.put(\"QA\", new int[] {2, 2, 1, 2});\n    countryGroupAssignment.put(\"RE\", new int[] {1, 0, 2, 2});\n    countryGroupAssignment.put(\"RO\", new int[] {0, 1, 1, 2});\n    countryGroupAssignment.put(\"RS\", new int[] {1, 2, 0, 0});\n    countryGroupAssignment.put(\"RU\", new int[] {0, 1, 1, 1});\n    countryGroupAssignment.put(\"RW\", new int[] {4, 4, 2, 4});\n    countryGroupAssignment.put(\"SA\", new int[] {2, 2, 2, 1});\n    countryGroupAssignment.put(\"SB\", new int[] {4, 4, 3, 0});\n    countryGroupAssignment.put(\"SC\", new int[] {4, 2, 0, 1});\n    countryGroupAssignment.put(\"SD\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"SE\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"SG\", new int[] {0, 2, 3, 3});\n    countryGroupAssignment.put(\"SH\", new int[] {4, 4, 2, 3});\n    countryGroupAssignment.put(\"SI\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"SJ\", new int[] {2, 0, 2, 4});\n    countryGroupAssignment.put(\"SK\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"SL\", new int[] {4, 3, 3, 3});\n    countryGroupAssignment.put(\"SM\", new int[] {0, 0, 2, 4});\n    countryGroupAssignment.put(\"SN\", new int[] {3, 4, 4, 2});\n    countryGroupAssignment.put(\"SO\", new int[] {3, 4, 4, 3});\n    countryGroupAssignment.put(\"SR\", new int[] {2, 2, 1, 0});\n    countryGroupAssignment.put(\"SS\", new int[] {4, 3, 4, 3});\n    countryGroupAssignment.put(\"ST\", new int[] {3, 4, 2, 2});\n    countryGroupAssignment.put(\"SV\", new int[] {2, 3, 3, 4});\n    countryGroupAssignment.put(\"SX\", new int[] {2, 4, 1, 0});\n    countryGroupAssignment.put(\"SY\", new int[] {4, 3, 2, 1});\n    countryGroupAssignment.put(\"SZ\", new int[] {4, 4, 3, 4});\n    countryGroupAssignment.put(\"TC\", new int[] {1, 2, 1, 1});\n    countryGroupAssignment.put(\"TD\", new int[] {4, 4, 4, 2});\n    countryGroupAssignment.put(\"TG\", new int[] {3, 3, 1, 0});\n    countryGroupAssignment.put(\"TH\", new int[] {1, 3, 4, 4});\n    countryGroupAssignment.put(\"TJ\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"TL\", new int[] {4, 2, 4, 4});\n    countryGroupAssignment.put(\"TM\", new int[] {4, 1, 2, 2});\n    countryGroupAssignment.put(\"TN\", new int[] {2, 2, 1, 2});\n    countryGroupAssignment.put(\"TO\", new int[] {3, 3, 3, 1});\n    countryGroupAssignment.put(\"TR\", new int[] {2, 2, 1, 2});\n    countryGroupAssignment.put(\"TT\", new int[] {1, 3, 1, 2});\n    countryGroupAssignment.put(\"TV\", new int[] {4, 2, 2, 4});\n    countryGroupAssignment.put(\"TW\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"TZ\", new int[] {3, 3, 4, 3});\n    countryGroupAssignment.put(\"UA\", new int[] {0, 2, 1, 2});\n    countryGroupAssignment.put(\"UG\", new int[] {4, 3, 3, 2});\n    countryGroupAssignment.put(\"US\", new int[] {1, 1, 3, 3});\n    countryGroupAssignment.put(\"UY\", new int[] {2, 2, 1, 1});\n    countryGroupAssignment.put(\"UZ\", new int[] {2, 2, 2, 2});\n    countryGroupAssignment.put(\"VA\", new int[] {1, 2, 4, 2});\n    countryGroupAssignment.put(\"VC\", new int[] {2, 0, 2, 4});\n    countryGroupAssignment.put(\"VE\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"VG\", new int[] {3, 0, 1, 3});\n    countryGroupAssignment.put(\"VI\", new int[] {1, 1, 4, 4});\n    countryGroupAssignment.put(\"VN\", new int[] {0, 2, 4, 4});\n    countryGroupAssignment.put(\"VU\", new int[] {4, 1, 3, 1});\n    countryGroupAssignment.put(\"WS\", new int[] {3, 3, 3, 2});\n    countryGroupAssignment.put(\"XK\", new int[] {1, 2, 1, 0});\n    countryGroupAssignment.put(\"YE\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"YT\", new int[] {2, 2, 2, 3});\n    countryGroupAssignment.put(\"ZA\", new int[] {2, 4, 2, 2});\n    countryGroupAssignment.put(\"ZM\", new int[] {3, 2, 2, 1});\n    countryGroupAssignment.put(\"ZW\", new int[] {3, 3, 2, 1});\n    return Collections.unmodifiableMap(countryGroupAssignment);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A {@link DataSource} that supports multiple URI schemes. The supported schemes are:\n *\n * <ul>\n *   <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just\n *       /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is\n *       a local file URI).\n *   <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).\n *   <li>rawresource: For fetching data from a raw resource in the application's apk (e.g.\n *       rawresource:///resourceId, where rawResourceId is the integer identifier of the raw\n *       resource).\n *   <li>content: For fetching data from a content URI (e.g. content://authority/path/123).\n *   <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an\n *       explicit dependency on ExoPlayer's RTMP extension.\n *   <li>data: For parsing data inlined in the URI as defined in RFC 2397.\n *   <li>udp: For fetching data over UDP (e.g. udp://something.com/media).\n *   <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),\n *       if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other\n *       schemes supported by a base data source if constructed using {@link\n *       #DefaultDataSource(Context, DataSource)}.\n * </ul>\n */\npublic final class DefaultDataSource implements DataSource {\n\n  private static final String TAG = \"DefaultDataSource\";\n\n  private static final String SCHEME_ASSET = \"asset\";\n  private static final String SCHEME_CONTENT = \"content\";\n  private static final String SCHEME_RTMP = \"rtmp\";\n  private static final String SCHEME_UDP = \"udp\";\n  private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;\n\n  private final Context context;\n  private final List<TransferListener> transferListeners;\n  private final DataSource baseDataSource;\n\n  // Lazily initialized.\n  @Nullable private DataSource fileDataSource;\n  @Nullable private DataSource assetDataSource;\n  @Nullable private DataSource contentDataSource;\n  @Nullable private DataSource rtmpDataSource;\n  @Nullable private DataSource udpDataSource;\n  @Nullable private DataSource dataSchemeDataSource;\n  @Nullable private DataSource rawResourceDataSource;\n\n  @Nullable private DataSource dataSource;\n\n  /**\n   * Constructs a new instance, optionally configured to follow cross-protocol redirects.\n   *\n   * @param context A context.\n   * @param userAgent The User-Agent to use when requesting remote data.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled when fetching remote data.\n   */\n  public DefaultDataSource(Context context, String userAgent, boolean allowCrossProtocolRedirects) {\n    this(\n        context,\n        userAgent,\n        DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,\n        allowCrossProtocolRedirects);\n  }\n\n  /**\n   * Constructs a new instance, optionally configured to follow cross-protocol redirects.\n   *\n   * @param context A context.\n   * @param userAgent The User-Agent to use when requesting remote data.\n   * @param connectTimeoutMillis The connection timeout that should be used when requesting remote\n   *     data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in\n   *     milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled when fetching remote data.\n   */\n  public DefaultDataSource(\n      Context context,\n      String userAgent,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects) {\n    this(\n        context,\n        new DefaultHttpDataSource(\n            userAgent,\n            connectTimeoutMillis,\n            readTimeoutMillis,\n            allowCrossProtocolRedirects,\n            /* defaultRequestProperties= */ null));\n  }\n\n  /**\n   * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other\n   * than file, asset and content.\n   *\n   * @param context A context.\n   * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and\n   *     content. This {@link DataSource} should normally support at least http(s).\n   */\n  public DefaultDataSource(Context context, DataSource baseDataSource) {\n    this.context = context.getApplicationContext();\n    this.baseDataSource = Assertions.checkNotNull(baseDataSource);\n    transferListeners = new ArrayList<>();\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    baseDataSource.addTransferListener(transferListener);\n    transferListeners.add(transferListener);\n    maybeAddListenerToDataSource(fileDataSource, transferListener);\n    maybeAddListenerToDataSource(assetDataSource, transferListener);\n    maybeAddListenerToDataSource(contentDataSource, transferListener);\n    maybeAddListenerToDataSource(rtmpDataSource, transferListener);\n    maybeAddListenerToDataSource(udpDataSource, transferListener);\n    maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);\n    maybeAddListenerToDataSource(rawResourceDataSource, transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    Assertions.checkState(dataSource == null);\n    // Choose the correct source for the scheme.\n    String scheme = dataSpec.uri.getScheme();\n    if (Util.isLocalFileUri(dataSpec.uri)) {\n      String uriPath = dataSpec.uri.getPath();\n      if (uriPath != null && uriPath.startsWith(\"/android_asset/\")) {\n        dataSource = getAssetDataSource();\n      } else {\n        dataSource = getFileDataSource();\n      }\n    } else if (SCHEME_ASSET.equals(scheme)) {\n      dataSource = getAssetDataSource();\n    } else if (SCHEME_CONTENT.equals(scheme)) {\n      dataSource = getContentDataSource();\n    } else if (SCHEME_RTMP.equals(scheme)) {\n      dataSource = getRtmpDataSource();\n    } else if (SCHEME_UDP.equals(scheme)) {\n      dataSource = getUdpDataSource();\n    } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {\n      dataSource = getDataSchemeDataSource();\n    } else if (SCHEME_RAW.equals(scheme)) {\n      dataSource = getRawResourceDataSource();\n    } else {\n      dataSource = baseDataSource;\n    }\n    // Open the source and return.\n    return dataSource.open(dataSpec);\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength);\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return dataSource == null ? null : dataSource.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (dataSource != null) {\n      try {\n        dataSource.close();\n      } finally {\n        dataSource = null;\n      }\n    }\n  }\n\n  private DataSource getUdpDataSource() {\n    if (udpDataSource == null) {\n      udpDataSource = new UdpDataSource();\n      addListenersToDataSource(udpDataSource);\n    }\n    return udpDataSource;\n  }\n\n  private DataSource getFileDataSource() {\n    if (fileDataSource == null) {\n      fileDataSource = new FileDataSource();\n      addListenersToDataSource(fileDataSource);\n    }\n    return fileDataSource;\n  }\n\n  private DataSource getAssetDataSource() {\n    if (assetDataSource == null) {\n      assetDataSource = new AssetDataSource(context);\n      addListenersToDataSource(assetDataSource);\n    }\n    return assetDataSource;\n  }\n\n  private DataSource getContentDataSource() {\n    if (contentDataSource == null) {\n      contentDataSource = new ContentDataSource(context);\n      addListenersToDataSource(contentDataSource);\n    }\n    return contentDataSource;\n  }\n\n  private DataSource getRtmpDataSource() {\n    if (rtmpDataSource == null) {\n      try {\n        // LINT.IfChange\n        Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.rtmp.RtmpDataSource\");\n        rtmpDataSource = (DataSource) clazz.getConstructor().newInstance();\n        // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n        addListenersToDataSource(rtmpDataSource);\n      } catch (ClassNotFoundException e) {\n        // Expected if the app was built without the RTMP extension.\n        Log.w(TAG, \"Attempting to play RTMP stream without depending on the RTMP extension\");\n      } catch (Exception e) {\n        // The RTMP extension is present, but instantiation failed.\n        throw new RuntimeException(\"Error instantiating RTMP extension\", e);\n      }\n      if (rtmpDataSource == null) {\n        rtmpDataSource = baseDataSource;\n      }\n    }\n    return rtmpDataSource;\n  }\n\n  private DataSource getDataSchemeDataSource() {\n    if (dataSchemeDataSource == null) {\n      dataSchemeDataSource = new DataSchemeDataSource();\n      addListenersToDataSource(dataSchemeDataSource);\n    }\n    return dataSchemeDataSource;\n  }\n\n  private DataSource getRawResourceDataSource() {\n    if (rawResourceDataSource == null) {\n      rawResourceDataSource = new RawResourceDataSource(context);\n      addListenersToDataSource(rawResourceDataSource);\n    }\n    return rawResourceDataSource;\n  }\n\n  private void addListenersToDataSource(DataSource dataSource) {\n    for (int i = 0; i < transferListeners.size(); i++) {\n      dataSource.addTransferListener(transferListeners.get(i));\n    }\n  }\n\n  private void maybeAddListenerToDataSource(\n      @Nullable DataSource dataSource, TransferListener listener) {\n    if (dataSource != null) {\n      dataSource.addTransferListener(listener);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.content.Context;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\n\n/**\n * A {@link Factory} that produces {@link DefaultDataSource} instances that delegate to\n * {@link DefaultHttpDataSource}s for non-file/asset/content URIs.\n */\npublic final class DefaultDataSourceFactory implements Factory {\n\n  private final Context context;\n  @Nullable private final TransferListener listener;\n  private final Factory baseDataSourceFactory;\n\n  /**\n   * @param context A context.\n   * @param userAgent The User-Agent string that should be used.\n   */\n  public DefaultDataSourceFactory(Context context, String userAgent) {\n    this(context, userAgent, /* listener= */ null);\n  }\n\n  /**\n   * @param context A context.\n   * @param userAgent The User-Agent string that should be used.\n   * @param listener An optional listener.\n   */\n  public DefaultDataSourceFactory(\n      Context context, String userAgent, @Nullable TransferListener listener) {\n    this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener));\n  }\n\n  /**\n   * @param context A context.\n   * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}\n   *     for {@link DefaultDataSource}.\n   * @see DefaultDataSource#DefaultDataSource(Context, DataSource)\n   */\n  public DefaultDataSourceFactory(Context context, Factory baseDataSourceFactory) {\n    this(context, /* listener= */ null, baseDataSourceFactory);\n  }\n\n  /**\n   * @param context A context.\n   * @param listener An optional listener.\n   * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}\n   *     for {@link DefaultDataSource}.\n   * @see DefaultDataSource#DefaultDataSource(Context, DataSource)\n   */\n  public DefaultDataSourceFactory(\n      Context context,\n      @Nullable TransferListener listener,\n      Factory baseDataSourceFactory) {\n    this.context = context.getApplicationContext();\n    this.listener = listener;\n    this.baseDataSourceFactory = baseDataSourceFactory;\n  }\n\n  @Override\n  public DefaultDataSource createDataSource() {\n    DefaultDataSource dataSource =\n        new DefaultDataSource(context, baseDataSourceFactory.createDataSource());\n    if (listener != null) {\n      dataSource.addTransferListener(listener);\n    }\n    return dataSource;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Predicate;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InterruptedIOException;\nimport java.io.OutputStream;\nimport java.lang.reflect.Method;\nimport java.net.HttpURLConnection;\nimport java.net.NoRouteToHostException;\nimport java.net.ProtocolException;\nimport java.net.URL;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.GZIPInputStream;\n\n/**\n * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.\n *\n * <p>By default this implementation will not follow cross-protocol redirects (i.e. redirects from\n * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link\n * #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} constructor and passing\n * {@code true} for the {@code allowCrossProtocolRedirects} argument.\n *\n * <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing\n * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to\n * construct the instance.\n */\npublic class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource {\n\n  /** The default connection timeout, in milliseconds. */\n  public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;\n  /**\n   * The default read timeout, in milliseconds.\n   */\n  public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;\n\n  private static final String TAG = \"DefaultHttpDataSource\";\n  private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.\n  private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;\n  private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;\n  private static final long MAX_BYTES_TO_DRAIN = 2048;\n  private static final Pattern CONTENT_RANGE_HEADER =\n      Pattern.compile(\"^bytes (\\\\d+)-(\\\\d+)/(\\\\d+)$\");\n  private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();\n\n  private final boolean allowCrossProtocolRedirects;\n  private final int connectTimeoutMillis;\n  private final int readTimeoutMillis;\n  private final String userAgent;\n  @Nullable private final RequestProperties defaultRequestProperties;\n  private final RequestProperties requestProperties;\n\n  @Nullable private Predicate<String> contentTypePredicate;\n  @Nullable private DataSpec dataSpec;\n  @Nullable private HttpURLConnection connection;\n  @Nullable private InputStream inputStream;\n  private boolean opened;\n  private int responseCode;\n\n  private long bytesToSkip;\n  private long bytesToRead;\n\n  private long bytesSkipped;\n  private long bytesRead;\n\n  /** @param userAgent The User-Agent string that should be used. */\n  public DefaultHttpDataSource(String userAgent) {\n    this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is\n   *     interpreted as an infinite timeout.\n   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as\n   *     an infinite timeout.\n   */\n  public DefaultHttpDataSource(String userAgent, int connectTimeoutMillis, int readTimeoutMillis) {\n    this(\n        userAgent,\n        connectTimeoutMillis,\n        readTimeoutMillis,\n        /* allowCrossProtocolRedirects= */ false,\n        /* defaultRequestProperties= */ null);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is\n   *     interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the\n   *     default value.\n   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as\n   *     an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled.\n   * @param defaultRequestProperties The default request properties to be sent to the server as HTTP\n   *     headers or {@code null} if not required.\n   */\n  public DefaultHttpDataSource(\n      String userAgent,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects,\n      @Nullable RequestProperties defaultRequestProperties) {\n    super(/* isNetwork= */ true);\n    this.userAgent = Assertions.checkNotEmpty(userAgent);\n    this.requestProperties = new RequestProperties();\n    this.connectTimeoutMillis = connectTimeoutMillis;\n    this.readTimeoutMillis = readTimeoutMillis;\n    this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;\n    this.defaultRequestProperties = defaultRequestProperties;\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then a {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link\n   *     #setContentTypePredicate(Predicate)}.\n   */\n  @Deprecated\n  public DefaultHttpDataSource(String userAgent, @Nullable Predicate<String> contentTypePredicate) {\n    this(\n        userAgent,\n        contentTypePredicate,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DEFAULT_READ_TIMEOUT_MILLIS);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then a {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is\n   *     interpreted as an infinite timeout.\n   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as\n   *     an infinite timeout.\n   * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link\n   *     #setContentTypePredicate(Predicate)}.\n   */\n  @SuppressWarnings(\"deprecation\")\n  @Deprecated\n  public DefaultHttpDataSource(\n      String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      int connectTimeoutMillis,\n      int readTimeoutMillis) {\n    this(\n        userAgent,\n        contentTypePredicate,\n        connectTimeoutMillis,\n        readTimeoutMillis,\n        /* allowCrossProtocolRedirects= */ false,\n        /* defaultRequestProperties= */ null);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then a {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is\n   *     interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the\n   *     default value.\n   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as\n   *     an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled.\n   * @param defaultRequestProperties The default request properties to be sent to the server as HTTP\n   *     headers or {@code null} if not required.\n   * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)}\n   *     and {@link #setContentTypePredicate(Predicate)}.\n   */\n  @Deprecated\n  public DefaultHttpDataSource(\n      String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects,\n      @Nullable RequestProperties defaultRequestProperties) {\n    super(/* isNetwork= */ true);\n    this.userAgent = Assertions.checkNotEmpty(userAgent);\n    this.contentTypePredicate = contentTypePredicate;\n    this.requestProperties = new RequestProperties();\n    this.connectTimeoutMillis = connectTimeoutMillis;\n    this.readTimeoutMillis = readTimeoutMillis;\n    this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;\n    this.defaultRequestProperties = defaultRequestProperties;\n  }\n\n  /**\n   * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a\n   * {@link InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.\n   *\n   * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a\n   *     predicate that was previously set.\n   */\n  public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {\n    this.contentTypePredicate = contentTypePredicate;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return connection == null ? null : Uri.parse(connection.getURL().toString());\n  }\n\n  @Override\n  public int getResponseCode() {\n    return connection == null || responseCode <= 0 ? -1 : responseCode;\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return connection == null ? Collections.emptyMap() : connection.getHeaderFields();\n  }\n\n  @Override\n  public void setRequestProperty(String name, String value) {\n    Assertions.checkNotNull(name);\n    Assertions.checkNotNull(value);\n    requestProperties.set(name, value);\n  }\n\n  @Override\n  public void clearRequestProperty(String name) {\n    Assertions.checkNotNull(name);\n    requestProperties.remove(name);\n  }\n\n  @Override\n  public void clearAllRequestProperties() {\n    requestProperties.clear();\n  }\n\n  /**\n   * Opens the source to read the specified data.\n   */\n  @Override\n  public long open(DataSpec dataSpec) throws HttpDataSourceException {\n    this.dataSpec = dataSpec;\n    this.bytesRead = 0;\n    this.bytesSkipped = 0;\n    transferInitializing(dataSpec);\n    try {\n      connection = makeConnection(dataSpec);\n    } catch (IOException e) {\n      throw new HttpDataSourceException(\"Unable to connect to \" + dataSpec.uri.toString(), e,\n          dataSpec, HttpDataSourceException.TYPE_OPEN);\n    }\n\n    String responseMessage;\n    try {\n      responseCode = connection.getResponseCode();\n      responseMessage = connection.getResponseMessage();\n    } catch (IOException e) {\n      closeConnectionQuietly();\n      throw new HttpDataSourceException(\"Unable to connect to \" + dataSpec.uri.toString(), e,\n          dataSpec, HttpDataSourceException.TYPE_OPEN);\n    }\n\n    // Check for a valid response code.\n    if (responseCode < 200 || responseCode > 299) {\n      Map<String, List<String>> headers = connection.getHeaderFields();\n      closeConnectionQuietly();\n      InvalidResponseCodeException exception =\n          new InvalidResponseCodeException(responseCode, responseMessage, headers, dataSpec);\n      if (responseCode == 416) {\n        exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));\n      }\n      throw exception;\n    }\n\n    // Check for a valid content type.\n    String contentType = connection.getContentType();\n    if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {\n      closeConnectionQuietly();\n      throw new InvalidContentTypeException(contentType, dataSpec);\n    }\n\n    // If we requested a range starting from a non-zero position and received a 200 rather than a\n    // 206, then the server does not support partial requests. We'll need to manually skip to the\n    // requested position.\n    bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;\n\n    // Determine the length of the data to be read, after skipping.\n    boolean isCompressed = isCompressed(connection);\n    if (!isCompressed) {\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesToRead = dataSpec.length;\n      } else {\n        long contentLength = getContentLength(connection);\n        bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)\n            : C.LENGTH_UNSET;\n      }\n    } else {\n      // Gzip is enabled. If the server opts to use gzip then the content length in the response\n      // will be that of the compressed data, which isn't what we want. Always use the dataSpec\n      // length in this case.\n      bytesToRead = dataSpec.length;\n    }\n\n    try {\n      inputStream = connection.getInputStream();\n      if (isCompressed) {\n        inputStream = new GZIPInputStream(inputStream);\n      }\n    } catch (IOException e) {\n      closeConnectionQuietly();\n      throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n\n    return bytesToRead;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {\n    try {\n      skipInternal();\n      return readInternal(buffer, offset, readLength);\n    } catch (IOException e) {\n      throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);\n    }\n  }\n\n  @Override\n  public void close() throws HttpDataSourceException {\n    try {\n      if (inputStream != null) {\n        maybeTerminateInputStream(connection, bytesRemaining());\n        try {\n          inputStream.close();\n        } catch (IOException e) {\n          throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);\n        }\n      }\n    } finally {\n      inputStream = null;\n      closeConnectionQuietly();\n      if (opened) {\n        opened = false;\n        transferEnded();\n      }\n    }\n  }\n\n  /**\n   * Returns the current connection, or null if the source is not currently opened.\n   *\n   * @return The current open connection, or null.\n   */\n  protected final @Nullable HttpURLConnection getConnection() {\n    return connection;\n  }\n\n  /**\n   * Returns the number of bytes that have been skipped since the most recent call to\n   * {@link #open(DataSpec)}.\n   *\n   * @return The number of bytes skipped.\n   */\n  protected final long bytesSkipped() {\n    return bytesSkipped;\n  }\n\n  /**\n   * Returns the number of bytes that have been read since the most recent call to\n   * {@link #open(DataSpec)}.\n   *\n   * @return The number of bytes read.\n   */\n  protected final long bytesRead() {\n    return bytesRead;\n  }\n\n  /**\n   * Returns the number of bytes that are still to be read for the current {@link DataSpec}.\n   * <p>\n   * If the total length of the data being read is known, then this length minus {@code bytesRead()}\n   * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.\n   *\n   * @return The remaining length, or {@link C#LENGTH_UNSET}.\n   */\n  protected final long bytesRemaining() {\n    return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;\n  }\n\n  /**\n   * Establishes a connection, following redirects to do so where permitted.\n   */\n  private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {\n    URL url = new URL(dataSpec.uri.toString());\n    @HttpMethod int httpMethod = dataSpec.httpMethod;\n    byte[] httpBody = dataSpec.httpBody;\n    long position = dataSpec.position;\n    long length = dataSpec.length;\n    boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);\n\n    if (!allowCrossProtocolRedirects) {\n      // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection\n      // automatically. This is the behavior we want, so use it.\n      return makeConnection(\n          url,\n          httpMethod,\n          httpBody,\n          position,\n          length,\n          allowGzip,\n          /* followRedirects= */ true,\n          dataSpec.httpRequestHeaders);\n    }\n\n    // We need to handle redirects ourselves to allow cross-protocol redirects.\n    int redirectCount = 0;\n    while (redirectCount++ <= MAX_REDIRECTS) {\n      HttpURLConnection connection =\n          makeConnection(\n              url,\n              httpMethod,\n              httpBody,\n              position,\n              length,\n              allowGzip,\n              /* followRedirects= */ false,\n              dataSpec.httpRequestHeaders);\n      int responseCode = connection.getResponseCode();\n      String location = connection.getHeaderField(\"Location\");\n      if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)\n          && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE\n              || responseCode == HttpURLConnection.HTTP_MOVED_PERM\n              || responseCode == HttpURLConnection.HTTP_MOVED_TEMP\n              || responseCode == HttpURLConnection.HTTP_SEE_OTHER\n              || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT\n              || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {\n        connection.disconnect();\n        url = handleRedirect(url, location);\n      } else if (httpMethod == DataSpec.HTTP_METHOD_POST\n          && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE\n              || responseCode == HttpURLConnection.HTTP_MOVED_PERM\n              || responseCode == HttpURLConnection.HTTP_MOVED_TEMP\n              || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) {\n        // POST request follows the redirect and is transformed into a GET request.\n        connection.disconnect();\n        httpMethod = DataSpec.HTTP_METHOD_GET;\n        httpBody = null;\n        url = handleRedirect(url, location);\n      } else {\n        return connection;\n      }\n    }\n\n    // If we get here we've been redirected more times than are permitted.\n    throw new NoRouteToHostException(\"Too many redirects: \" + redirectCount);\n  }\n\n  /**\n   * Configures a connection and opens it.\n   *\n   * @param url The url to connect to.\n   * @param httpMethod The http method.\n   * @param httpBody The body data.\n   * @param position The byte offset of the requested data.\n   * @param length The length of the requested data, or {@link C#LENGTH_UNSET}.\n   * @param allowGzip Whether to allow the use of gzip.\n   * @param followRedirects Whether to follow redirects.\n   * @param requestParameters parameters (HTTP headers) to include in request.\n   */\n  private HttpURLConnection makeConnection(\n      URL url,\n      @HttpMethod int httpMethod,\n      byte[] httpBody,\n      long position,\n      long length,\n      boolean allowGzip,\n      boolean followRedirects,\n      Map<String, String> requestParameters)\n      throws IOException {\n    HttpURLConnection connection = openConnection(url);\n    connection.setConnectTimeout(connectTimeoutMillis);\n    connection.setReadTimeout(readTimeoutMillis);\n\n    Map<String, String> requestHeaders = new HashMap<>();\n    if (defaultRequestProperties != null) {\n      requestHeaders.putAll(defaultRequestProperties.getSnapshot());\n    }\n    requestHeaders.putAll(requestProperties.getSnapshot());\n    requestHeaders.putAll(requestParameters);\n\n    for (Map.Entry<String, String> property : requestHeaders.entrySet()) {\n      connection.setRequestProperty(property.getKey(), property.getValue());\n    }\n\n    if (!(position == 0 && length == C.LENGTH_UNSET)) {\n      String rangeRequest = \"bytes=\" + position + \"-\";\n      if (length != C.LENGTH_UNSET) {\n        rangeRequest += (position + length - 1);\n      }\n      connection.setRequestProperty(\"Range\", rangeRequest);\n    }\n    connection.setRequestProperty(\"User-Agent\", userAgent);\n    connection.setRequestProperty(\"Accept-Encoding\", allowGzip ? \"gzip\" : \"identity\");\n    connection.setInstanceFollowRedirects(followRedirects);\n    connection.setDoOutput(httpBody != null);\n    connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));\n    \n    if (httpBody != null) {\n      connection.setFixedLengthStreamingMode(httpBody.length);\n      connection.connect();\n      OutputStream os = connection.getOutputStream();\n      os.write(httpBody);\n      os.close();\n    } else {\n      connection.connect();\n    }\n    return connection;\n  }\n\n  /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */\n  @VisibleForTesting\n  /* package */ HttpURLConnection openConnection(URL url) throws IOException {\n    return (HttpURLConnection) url.openConnection();\n  }\n\n  /**\n   * Handles a redirect.\n   *\n   * @param originalUrl The original URL.\n   * @param location The Location header in the response.\n   * @return The next URL.\n   * @throws IOException If redirection isn't possible.\n   */\n  private static URL handleRedirect(URL originalUrl, String location) throws IOException {\n    if (location == null) {\n      throw new ProtocolException(\"Null location redirect\");\n    }\n    // Form the new url.\n    URL url = new URL(originalUrl, location);\n    // Check that the protocol of the new url is supported.\n    String protocol = url.getProtocol();\n    if (!\"https\".equals(protocol) && !\"http\".equals(protocol)) {\n      throw new ProtocolException(\"Unsupported protocol redirect: \" + protocol);\n    }\n    // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code\n    // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol\n    // redirects are disabled, we'll need to uncomment this block of code.\n    // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {\n    //   throw new ProtocolException(\"Disallowed cross-protocol redirect (\"\n    //       + originalUrl.getProtocol() + \" to \" + protocol + \")\");\n    // }\n    return url;\n  }\n\n  /**\n   * Attempts to extract the length of the content from the response headers of an open connection.\n   *\n   * @param connection The open connection.\n   * @return The extracted length, or {@link C#LENGTH_UNSET}.\n   */\n  private static long getContentLength(HttpURLConnection connection) {\n    long contentLength = C.LENGTH_UNSET;\n    String contentLengthHeader = connection.getHeaderField(\"Content-Length\");\n    if (!TextUtils.isEmpty(contentLengthHeader)) {\n      try {\n        contentLength = Long.parseLong(contentLengthHeader);\n      } catch (NumberFormatException e) {\n        Log.e(TAG, \"Unexpected Content-Length [\" + contentLengthHeader + \"]\");\n      }\n    }\n    String contentRangeHeader = connection.getHeaderField(\"Content-Range\");\n    if (!TextUtils.isEmpty(contentRangeHeader)) {\n      Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);\n      if (matcher.find()) {\n        try {\n          long contentLengthFromRange =\n              Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;\n          if (contentLength < 0) {\n            // Some proxy servers strip the Content-Length header. Fall back to the length\n            // calculated here in this case.\n            contentLength = contentLengthFromRange;\n          } else if (contentLength != contentLengthFromRange) {\n            // If there is a discrepancy between the Content-Length and Content-Range headers,\n            // assume the one with the larger value is correct. We have seen cases where carrier\n            // change one of them to reduce the size of a request, but it is unlikely anybody would\n            // increase it.\n            Log.w(TAG, \"Inconsistent headers [\" + contentLengthHeader + \"] [\" + contentRangeHeader\n                + \"]\");\n            contentLength = Math.max(contentLength, contentLengthFromRange);\n          }\n        } catch (NumberFormatException e) {\n          Log.e(TAG, \"Unexpected Content-Range [\" + contentRangeHeader + \"]\");\n        }\n      }\n    }\n    return contentLength;\n  }\n\n  /**\n   * Skips any bytes that need skipping. Else does nothing.\n   * <p>\n   * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.\n   *\n   * @throws InterruptedIOException If the thread is interrupted during the operation.\n   * @throws EOFException If the end of the input stream is reached before the bytes are skipped.\n   */\n  private void skipInternal() throws IOException {\n    if (bytesSkipped == bytesToSkip) {\n      return;\n    }\n\n    // Acquire the shared skip buffer.\n    byte[] skipBuffer = skipBufferReference.getAndSet(null);\n    if (skipBuffer == null) {\n      skipBuffer = new byte[4096];\n    }\n\n    while (bytesSkipped != bytesToSkip) {\n      int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);\n      int read = inputStream.read(skipBuffer, 0, readLength);\n      if (Thread.currentThread().isInterrupted()) {\n        throw new InterruptedIOException();\n      }\n      if (read == -1) {\n        throw new EOFException();\n      }\n      bytesSkipped += read;\n      bytesTransferred(read);\n    }\n\n    // Release the shared skip buffer.\n    skipBufferReference.set(skipBuffer);\n  }\n\n  /**\n   * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at\n   * index {@code offset}.\n   * <p>\n   * This method blocks until at least one byte of data can be read, the end of the opened range is\n   * detected, or an exception is thrown.\n   *\n   * @param buffer The buffer into which the read data should be stored.\n   * @param offset The start offset into {@code buffer} at which data should be written.\n   * @param readLength The maximum number of bytes to read.\n   * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened\n   *     range is reached.\n   * @throws IOException If an error occurs reading from the source.\n   */\n  private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {\n    if (readLength == 0) {\n      return 0;\n    }\n    if (bytesToRead != C.LENGTH_UNSET) {\n      long bytesRemaining = bytesToRead - bytesRead;\n      if (bytesRemaining == 0) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      readLength = (int) Math.min(readLength, bytesRemaining);\n    }\n\n    int read = inputStream.read(buffer, offset, readLength);\n    if (read == -1) {\n      if (bytesToRead != C.LENGTH_UNSET) {\n        // End of stream reached having not read sufficient data.\n        throw new EOFException();\n      }\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    bytesRead += read;\n    bytesTransferred(read);\n    return read;\n  }\n\n  /**\n   * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can\n   * block for a long time if the stream has a lot of data remaining. Call this method before\n   * closing the input stream to make a best effort to cause the input stream to encounter an\n   * unexpected end of input, working around this issue. On other platform API levels, the method\n   * does nothing.\n   *\n   * @param connection The connection whose {@link InputStream} should be terminated.\n   * @param bytesRemaining The number of bytes remaining to be read from the input stream if its\n   *     length is known. {@link C#LENGTH_UNSET} otherwise.\n   */\n  private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {\n    if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {\n      return;\n    }\n\n    try {\n      InputStream inputStream = connection.getInputStream();\n      if (bytesRemaining == C.LENGTH_UNSET) {\n        // If the input stream has already ended, do nothing. The socket may be re-used.\n        if (inputStream.read() == -1) {\n          return;\n        }\n      } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {\n        // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be\n        // re-used.\n        return;\n      }\n      String className = inputStream.getClass().getName();\n      if (\"com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream\".equals(className)\n          || \"com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream\"\n              .equals(className)) {\n        Class<?> superclass = inputStream.getClass().getSuperclass();\n        Method unexpectedEndOfInput = superclass.getDeclaredMethod(\"unexpectedEndOfInput\");\n        unexpectedEndOfInput.setAccessible(true);\n        unexpectedEndOfInput.invoke(inputStream);\n      }\n    } catch (Exception e) {\n      // If an IOException then the connection didn't ever have an input stream, or it was closed\n      // already. If another type of exception then something went wrong, most likely the device\n      // isn't using okhttp.\n    }\n  }\n\n\n  /**\n   * Closes the current connection quietly, if there is one.\n   */\n  private void closeConnectionQuietly() {\n    if (connection != null) {\n      try {\n        connection.disconnect();\n      } catch (Exception e) {\n        Log.e(TAG, \"Unexpected error while disconnecting\", e);\n      }\n      connection = null;\n    }\n  }\n\n  private static boolean isCompressed(HttpURLConnection connection) {\n    String contentEncoding = connection.getHeaderField(\"Content-Encoding\");\n    return \"gzip\".equalsIgnoreCase(contentEncoding);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.Factory;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */\npublic final class DefaultHttpDataSourceFactory extends BaseFactory {\n\n  private final String userAgent;\n  @Nullable private final TransferListener listener;\n  private final int connectTimeoutMillis;\n  private final int readTimeoutMillis;\n  private final boolean allowCrossProtocolRedirects;\n\n  /**\n   * Constructs a DefaultHttpDataSourceFactory. Sets {@link\n   * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link\n   * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param userAgent The User-Agent string that should be used.\n   */\n  public DefaultHttpDataSourceFactory(String userAgent) {\n    this(userAgent, null);\n  }\n\n  /**\n   * Constructs a DefaultHttpDataSourceFactory. Sets {@link\n   * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link\n   * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param userAgent The User-Agent string that should be used.\n   * @param listener An optional listener.\n   * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean)\n   */\n  public DefaultHttpDataSourceFactory(String userAgent, @Nullable TransferListener listener) {\n    this(userAgent, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param connectTimeoutMillis The connection timeout that should be used when requesting remote\n   *     data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in\n   *     milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled.\n   */\n  public DefaultHttpDataSourceFactory(\n      String userAgent,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects) {\n    this(\n        userAgent,\n        /* listener= */ null,\n        connectTimeoutMillis,\n        readTimeoutMillis,\n        allowCrossProtocolRedirects);\n  }\n\n  /**\n   * @param userAgent The User-Agent string that should be used.\n   * @param listener An optional listener.\n   * @param connectTimeoutMillis The connection timeout that should be used when requesting remote\n   *     data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in\n   *     milliseconds. A timeout of zero is interpreted as an infinite timeout.\n   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP\n   *     to HTTPS and vice versa) are enabled.\n   */\n  public DefaultHttpDataSourceFactory(\n      String userAgent,\n      @Nullable TransferListener listener,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects) {\n    this.userAgent = Assertions.checkNotEmpty(userAgent);\n    this.listener = listener;\n    this.connectTimeoutMillis = connectTimeoutMillis;\n    this.readTimeoutMillis = readTimeoutMillis;\n    this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;\n  }\n\n  @Override\n  protected DefaultHttpDataSource createDataSourceInternal(\n      HttpDataSource.RequestProperties defaultRequestProperties) {\n    DefaultHttpDataSource dataSource =\n        new DefaultHttpDataSource(\n            userAgent,\n            connectTimeoutMillis,\n            readTimeoutMillis,\n            allowCrossProtocolRedirects,\n            defaultRequestProperties);\n    if (listener != null) {\n      dataSource.addTransferListener(listener);\n    }\n    return dataSource;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\nimport com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\n\n/** Default implementation of {@link LoadErrorHandlingPolicy}. */\npublic class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {\n\n  /** The default minimum number of times to retry loading data prior to propagating the error. */\n  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;\n  /**\n   * The default minimum number of times to retry loading prior to failing for progressive live\n   * streams.\n   */\n  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE = 6;\n  /** The default duration for which a track is blacklisted in milliseconds. */\n  public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000;\n\n  private static final int DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT = -1;\n\n  private final int minimumLoadableRetryCount;\n\n  /**\n   * Creates an instance with default behavior.\n   *\n   * <p>{@link #getMinimumLoadableRetryCount} will return {@link\n   * #DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE} for {@code dataType} {@link\n   * C#DATA_TYPE_MEDIA_PROGRESSIVE_LIVE}. For other {@code dataType} values, it will return {@link\n   * #DEFAULT_MIN_LOADABLE_RETRY_COUNT}.\n   */\n  public DefaultLoadErrorHandlingPolicy() {\n    this(DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT);\n  }\n\n  /**\n   * Creates an instance with the given value for {@link #getMinimumLoadableRetryCount(int)}.\n   *\n   * @param minimumLoadableRetryCount See {@link #getMinimumLoadableRetryCount}.\n   */\n  public DefaultLoadErrorHandlingPolicy(int minimumLoadableRetryCount) {\n    this.minimumLoadableRetryCount = minimumLoadableRetryCount;\n  }\n\n  /**\n   * Blacklists resources whose load error was an {@link InvalidResponseCodeException} with response\n   * code HTTP 404 or 410. The duration of the blacklisting is {@link #DEFAULT_TRACK_BLACKLIST_MS}.\n   */\n  @Override\n  public long getBlacklistDurationMsFor(\n      int dataType, long loadDurationMs, IOException exception, int errorCount) {\n    if (exception instanceof InvalidResponseCodeException) {\n      int responseCode = ((InvalidResponseCodeException) exception).responseCode;\n      return responseCode == 404 // HTTP 404 Not Found.\n              || responseCode == 410 // HTTP 410 Gone.\n              || responseCode == 416 // HTTP 416 Range Not Satisfiable.\n          ? DEFAULT_TRACK_BLACKLIST_MS\n          : C.TIME_UNSET;\n    }\n    return C.TIME_UNSET;\n  }\n\n  /**\n   * Retries for any exception that is not a subclass of {@link ParserException}, {@link\n   * FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as\n   * {@code Math.min((errorCount - 1) * 1000, 5000)}.\n   */\n  @Override\n  public long getRetryDelayMsFor(\n      int dataType, long loadDurationMs, IOException exception, int errorCount) {\n    return exception instanceof ParserException\n            || exception instanceof FileNotFoundException\n            || exception instanceof UnexpectedLoaderException\n        ? C.TIME_UNSET\n        : Math.min((errorCount - 1) * 1000, 5000);\n  }\n\n  /**\n   * See {@link #DefaultLoadErrorHandlingPolicy()} and {@link #DefaultLoadErrorHandlingPolicy(int)}\n   * for documentation about the behavior of this method.\n   */\n  @Override\n  public int getMinimumLoadableRetryCount(int dataType) {\n    if (minimumLoadableRetryCount == DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT) {\n      return dataType == C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE\n          ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE\n          : DEFAULT_MIN_LOADABLE_RETRY_COUNT;\n    } else {\n      return minimumLoadableRetryCount;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport java.io.IOException;\n\n/**\n * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}.\n */\npublic final class DummyDataSource implements DataSource {\n\n  public static final DummyDataSource INSTANCE = new DummyDataSource();\n\n  /** A factory that produces {@link DummyDataSource}. */\n  public static final Factory FACTORY = DummyDataSource::new;\n\n  private DummyDataSource() {}\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    // Do nothing.\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    throw new IOException(\"Dummy source\");\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return null;\n  }\n\n  @Override\n  public void close() {\n    // do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.EOFException;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\n\n/** A {@link DataSource} for reading local files. */\npublic final class FileDataSource extends BaseDataSource {\n\n  /** Thrown when a {@link FileDataSource} encounters an error reading a file. */\n  public static class FileDataSourceException extends IOException {\n\n    public FileDataSourceException(IOException cause) {\n      super(cause);\n    }\n\n    public FileDataSourceException(String message, IOException cause) {\n      super(message, cause);\n    }\n  }\n\n  /** {@link DataSource.Factory} for {@link FileDataSource} instances. */\n  public static final class Factory implements DataSource.Factory {\n\n    @Nullable private TransferListener listener;\n\n    /**\n     * Sets a {@link TransferListener} for {@link FileDataSource} instances created by this factory.\n     *\n     * @param listener The {@link TransferListener}.\n     * @return This factory.\n     */\n    public Factory setListener(@Nullable TransferListener listener) {\n      this.listener = listener;\n      return this;\n    }\n\n    @Override\n    public FileDataSource createDataSource() {\n      FileDataSource dataSource = new FileDataSource();\n      if (listener != null) {\n        dataSource.addTransferListener(listener);\n      }\n      return dataSource;\n    }\n  }\n\n  @Nullable private RandomAccessFile file;\n  @Nullable private Uri uri;\n  private long bytesRemaining;\n  private boolean opened;\n\n  public FileDataSource() {\n    super(/* isNetwork= */ false);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws FileDataSourceException {\n    try {\n      Uri uri = dataSpec.uri;\n      this.uri = uri;\n\n      transferInitializing(dataSpec);\n\n      this.file = openLocalFile(uri);\n\n      file.seek(dataSpec.position);\n      bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position\n          : dataSpec.length;\n      if (bytesRemaining < 0) {\n        throw new EOFException();\n      }\n    } catch (IOException e) {\n      throw new FileDataSourceException(e);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n\n    return bytesRemaining;\n  }\n\n  private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException {\n    try {\n      return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), \"r\");\n    } catch (FileNotFoundException e) {\n      if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) {\n        throw new FileDataSourceException(\n            String.format(\n                \"uri has query and/or fragment, which are not supported. Did you call Uri.parse()\"\n                    + \" on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to\"\n                    + \" avoid this. path=%s,query=%s,fragment=%s\",\n                uri.getPath(), uri.getQuery(), uri.getFragment()),\n            e);\n      }\n      throw new FileDataSourceException(e);\n    }\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    } else {\n      int bytesRead;\n      try {\n        bytesRead =\n            castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength));\n      } catch (IOException e) {\n        throw new FileDataSourceException(e);\n      }\n\n      if (bytesRead > 0) {\n        bytesRemaining -= bytesRead;\n        bytesTransferred(bytesRead);\n      }\n\n      return bytesRead;\n    }\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public void close() throws FileDataSourceException {\n    uri = null;\n    try {\n      if (file != null) {\n        file.close();\n      }\n    } catch (IOException e) {\n      throw new FileDataSourceException(e);\n    } finally {\n      file = null;\n      if (opened) {\n        opened = false;\n        transferEnded();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport androidx.annotation.Nullable;\n\n/** @deprecated Use {@link FileDataSource.Factory}. */\n@Deprecated\npublic final class FileDataSourceFactory implements DataSource.Factory {\n\n  private final FileDataSource.Factory wrappedFactory;\n\n  public FileDataSourceFactory() {\n    this(/* listener= */ null);\n  }\n\n  public FileDataSourceFactory(@Nullable TransferListener listener) {\n    wrappedFactory = new FileDataSource.Factory().setListener(listener);\n  }\n\n  @Override\n  public FileDataSource createDataSource() {\n    return wrappedFactory.createDataSource();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.text.TextUtils;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Predicate;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * An HTTP {@link DataSource}.\n */\npublic interface HttpDataSource extends DataSource {\n\n  /**\n   * A factory for {@link HttpDataSource} instances.\n   */\n  interface Factory extends DataSource.Factory {\n\n    @Override\n    HttpDataSource createDataSource();\n\n    /**\n     * Gets the default request properties used by all {@link HttpDataSource}s created by the\n     * factory. Changes to the properties will be reflected in any future requests made by\n     * {@link HttpDataSource}s created by the factory.\n     *\n     * @return The default request properties of the factory.\n     */\n    RequestProperties getDefaultRequestProperties();\n\n    /**\n     * Sets a default request header for {@link HttpDataSource} instances created by the factory.\n     *\n     * @deprecated Use {@link #getDefaultRequestProperties} instead.\n     * @param name The name of the header field.\n     * @param value The value of the field.\n     */\n    @Deprecated\n    void setDefaultRequestProperty(String name, String value);\n\n    /**\n     * Clears a default request header for {@link HttpDataSource} instances created by the factory.\n     *\n     * @deprecated Use {@link #getDefaultRequestProperties} instead.\n     * @param name The name of the header field.\n     */\n    @Deprecated\n    void clearDefaultRequestProperty(String name);\n\n    /**\n     * Clears all default request headers for all {@link HttpDataSource} instances created by the\n     * factory.\n     *\n     * @deprecated Use {@link #getDefaultRequestProperties} instead.\n     */\n    @Deprecated\n    void clearAllDefaultRequestProperties();\n\n  }\n\n  /**\n   * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers\n   * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or\n   * unintended state.\n   */\n  final class RequestProperties {\n\n    private final Map<String, String> requestProperties;\n    private Map<String, String> requestPropertiesSnapshot;\n\n    public RequestProperties() {\n      requestProperties = new HashMap<>();\n    }\n\n    /**\n     * Sets the specified property {@code value} for the specified {@code name}. If a property for\n     * this name previously existed, the old value is replaced by the specified value.\n     *\n     * @param name The name of the request property.\n     * @param value The value of the request property.\n     */\n    public synchronized void set(String name, String value) {\n      requestPropertiesSnapshot = null;\n      requestProperties.put(name, value);\n    }\n\n    /**\n     * Sets the keys and values contained in the map. If a property previously existed, the old\n     * value is replaced by the specified value. If a property previously existed and is not in the\n     * map, the property is left unchanged.\n     *\n     * @param properties The request properties.\n     */\n    public synchronized void set(Map<String, String> properties) {\n      requestPropertiesSnapshot = null;\n      requestProperties.putAll(properties);\n    }\n\n    /**\n     * Removes all properties previously existing and sets the keys and values of the map.\n     *\n     * @param properties The request properties.\n     */\n    public synchronized void clearAndSet(Map<String, String> properties) {\n      requestPropertiesSnapshot = null;\n      requestProperties.clear();\n      requestProperties.putAll(properties);\n    }\n\n    /**\n     * Removes a request property by name.\n     *\n     * @param name The name of the request property to remove.\n     */\n    public synchronized void remove(String name) {\n      requestPropertiesSnapshot = null;\n      requestProperties.remove(name);\n    }\n\n    /**\n     * Clears all request properties.\n     */\n    public synchronized void clear() {\n      requestPropertiesSnapshot = null;\n      requestProperties.clear();\n    }\n\n    /**\n     * Gets a snapshot of the request properties.\n     *\n     * @return A snapshot of the request properties.\n     */\n    public synchronized Map<String, String> getSnapshot() {\n      if (requestPropertiesSnapshot == null) {\n        requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties));\n      }\n      return requestPropertiesSnapshot;\n    }\n\n  }\n\n  /**\n   * Base implementation of {@link Factory} that sets default request properties.\n   */\n  abstract class BaseFactory implements Factory {\n\n    private final RequestProperties defaultRequestProperties;\n\n    public BaseFactory() {\n      defaultRequestProperties = new RequestProperties();\n    }\n\n    @Override\n    public final HttpDataSource createDataSource() {\n      return createDataSourceInternal(defaultRequestProperties);\n    }\n\n    @Override\n    public final RequestProperties getDefaultRequestProperties() {\n      return defaultRequestProperties;\n    }\n\n    /** @deprecated Use {@link #getDefaultRequestProperties} instead. */\n    @Deprecated\n    @Override\n    public final void setDefaultRequestProperty(String name, String value) {\n      defaultRequestProperties.set(name, value);\n    }\n\n    /** @deprecated Use {@link #getDefaultRequestProperties} instead. */\n    @Deprecated\n    @Override\n    public final void clearDefaultRequestProperty(String name) {\n      defaultRequestProperties.remove(name);\n    }\n\n    /** @deprecated Use {@link #getDefaultRequestProperties} instead. */\n    @Deprecated\n    @Override\n    public final void clearAllDefaultRequestProperties() {\n      defaultRequestProperties.clear();\n    }\n\n    /**\n     * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance.\n     *\n     * @param defaultRequestProperties The default {@code RequestProperties} to be used by the\n     *     {@link HttpDataSource} instance.\n     * @return A {@link HttpDataSource} instance.\n     */\n    protected abstract HttpDataSource createDataSourceInternal(RequestProperties\n        defaultRequestProperties);\n\n  }\n\n  /** A {@link Predicate} that rejects content types often used for pay-walls. */\n  Predicate<String> REJECT_PAYWALL_TYPES =\n      contentType -> {\n        contentType = Util.toLowerInvariant(contentType);\n        return !TextUtils.isEmpty(contentType)\n            && (!contentType.contains(\"text\") || contentType.contains(\"text/vtt\"))\n            && !contentType.contains(\"html\")\n            && !contentType.contains(\"xml\");\n      };\n\n  /**\n   * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}.\n   */\n  class HttpDataSourceException extends IOException {\n\n    @Documented\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({TYPE_OPEN, TYPE_READ, TYPE_CLOSE})\n    public @interface Type {}\n\n    public static final int TYPE_OPEN = 1;\n    public static final int TYPE_READ = 2;\n    public static final int TYPE_CLOSE = 3;\n\n    @Type public final int type;\n\n    /**\n     * The {@link DataSpec} associated with the current connection.\n     */\n    public final DataSpec dataSpec;\n\n    public HttpDataSourceException(DataSpec dataSpec, @Type int type) {\n      super();\n      this.dataSpec = dataSpec;\n      this.type = type;\n    }\n\n    public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) {\n      super(message);\n      this.dataSpec = dataSpec;\n      this.type = type;\n    }\n\n    public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) {\n      super(cause);\n      this.dataSpec = dataSpec;\n      this.type = type;\n    }\n\n    public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec,\n        @Type int type) {\n      super(message, cause);\n      this.dataSpec = dataSpec;\n      this.type = type;\n    }\n\n  }\n\n  /**\n   * Thrown when the content type is invalid.\n   */\n  final class InvalidContentTypeException extends HttpDataSourceException {\n\n    public final String contentType;\n\n    public InvalidContentTypeException(String contentType, DataSpec dataSpec) {\n      super(\"Invalid content type: \" + contentType, dataSpec, TYPE_OPEN);\n      this.contentType = contentType;\n    }\n\n  }\n\n  /**\n   * Thrown when an attempt to open a connection results in a response code not in the 2xx range.\n   */\n  final class InvalidResponseCodeException extends HttpDataSourceException {\n\n    /**\n     * The response code that was outside of the 2xx range.\n     */\n    public final int responseCode;\n\n    /** The http status message. */\n    @Nullable public final String responseMessage;\n\n    /**\n     * An unmodifiable map of the response header fields and values.\n     */\n    public final Map<String, List<String>> headerFields;\n\n    /** @deprecated Use {@link #InvalidResponseCodeException(int, String, Map, DataSpec)}. */\n    @Deprecated\n    public InvalidResponseCodeException(\n        int responseCode, Map<String, List<String>> headerFields, DataSpec dataSpec) {\n      this(responseCode, /* responseMessage= */ null, headerFields, dataSpec);\n    }\n\n    public InvalidResponseCodeException(\n        int responseCode,\n        @Nullable String responseMessage,\n        Map<String, List<String>> headerFields,\n        DataSpec dataSpec) {\n      super(\"Response code: \" + responseCode, dataSpec, TYPE_OPEN);\n      this.responseCode = responseCode;\n      this.responseMessage = responseMessage;\n      this.headerFields = headerFields;\n    }\n\n  }\n\n  /**\n   * Opens the source to read the specified data.\n   *\n   * <p>Note: {@link HttpDataSource} implementations are advised to set request headers passed via\n   * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the\n   * default parameters set in the {@link Factory}.\n   */\n  @Override\n  long open(DataSpec dataSpec) throws HttpDataSourceException;\n\n  @Override\n  void close() throws HttpDataSourceException;\n\n  @Override\n  int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException;\n\n  /**\n   * Sets the value of a request header. The value will be used for subsequent connections\n   * established by the source.\n   *\n   * <p>Note: If the same header is set as a default parameter in the {@link Factory}, then the\n   * header value set with this method should be preferred when connecting with the data source. See\n   * {@link #open}.\n   *\n   * @param name The name of the header field.\n   * @param value The value of the field.\n   */\n  void setRequestProperty(String name, String value);\n\n  /**\n   * Clears the value of a request header. The change will apply to subsequent connections\n   * established by the source.\n   *\n   * @param name The name of the header field.\n   */\n  void clearRequestProperty(String name);\n\n  /**\n   * Clears all request headers that were set by {@link #setRequestProperty(String, String)}.\n   */\n  void clearAllRequestProperties();\n\n  /**\n   * When the source is open, returns the HTTP response status code associated with the last {@link\n   * #open} call. Otherwise, returns a negative value.\n   */\n  int getResponseCode();\n\n  @Override\n  Map<String, List<String>> getResponseHeaders();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.Loader.Callback;\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport java.io.IOException;\n\n/**\n * Defines how errors encountered by {@link Loader Loaders} are handled.\n *\n * <p>Loader clients may blacklist a resource when a load error occurs. Blacklisting works around\n * load errors by loading an alternative resource. Clients do not try blacklisting when a resource\n * does not have an alternative. When a resource does have valid alternatives, {@link\n * #getBlacklistDurationMsFor(int, long, IOException, int)} defines whether the resource should be\n * blacklisted. Blacklisting will succeed if any of the alternatives is not in the black list.\n *\n * <p>When blacklisting does not take place, {@link #getRetryDelayMsFor(int, long, IOException,\n * int)} defines whether the load is retried. Errors whose load is not retried are propagated. Load\n * errors whose load is retried are propagated according to {@link\n * #getMinimumLoadableRetryCount(int)}.\n *\n * <p>Methods are invoked on the playback thread.\n */\npublic interface LoadErrorHandlingPolicy {\n\n  /**\n   * Returns the number of milliseconds for which a resource associated to a provided load error\n   * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted.\n   *\n   * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to\n   *     load.\n   * @param loadDurationMs The duration in milliseconds of the load from the start of the first load\n   *     attempt up to the point at which the error occurred.\n   * @param exception The load error.\n   * @param errorCount The number of errors this load has encountered, including this one.\n   * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should\n   *     not be blacklisted.\n   */\n  long getBlacklistDurationMsFor(\n          int dataType, long loadDurationMs, IOException exception, int errorCount);\n\n  /**\n   * Returns the number of milliseconds to wait before attempting the load again, or {@link\n   * C#TIME_UNSET} if the error is fatal and should not be retried.\n   *\n   * <p>{@link Loader} clients may ignore the retry delay returned by this method in order to wait\n   * for a specific event before retrying. However, the load is retried if and only if this method\n   * does not return {@link C#TIME_UNSET}.\n   *\n   * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to\n   *     load.\n   * @param loadDurationMs The duration in milliseconds of the load from the start of the first load\n   *     attempt up to the point at which the error occurred.\n   * @param exception The load error.\n   * @param errorCount The number of errors this load has encountered, including this one.\n   * @return The number of milliseconds to wait before attempting the load again, or {@link\n   *     C#TIME_UNSET} if the error is fatal and should not be retried.\n   */\n  long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount);\n\n  /**\n   * Returns the minimum number of times to retry a load in the case of a load error, before\n   * propagating the error.\n   *\n   * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to\n   *     load.\n   * @return The minimum number of times to retry a load in the case of a load error, before\n   *     propagating the error.\n   * @see Loader#startLoading(Loadable, Callback, int)\n   */\n  int getMinimumLoadableRetryCount(int dataType);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/Loader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.annotation.SuppressLint;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.concurrent.ExecutorService;\n\n/**\n * Manages the background loading of {@link Loadable}s.\n */\npublic final class Loader implements LoaderErrorThrower {\n\n  /**\n   * Thrown when an unexpected exception or error is encountered during loading.\n   */\n  public static final class UnexpectedLoaderException extends IOException {\n\n    public UnexpectedLoaderException(Throwable cause) {\n      super(\"Unexpected \" + cause.getClass().getSimpleName() + \": \" + cause.getMessage(), cause);\n    }\n\n  }\n\n  /**\n   * An object that can be loaded using a {@link Loader}.\n   */\n  public interface Loadable {\n\n    /**\n     * Cancels the load.\n     */\n    void cancelLoad();\n\n    /**\n     * Performs the load, returning on completion or cancellation.\n     *\n     * @throws IOException If the input could not be loaded.\n     * @throws InterruptedException If the thread was interrupted.\n     */\n    void load() throws IOException, InterruptedException;\n\n  }\n\n  /**\n   * A callback to be notified of {@link Loader} events.\n   */\n  public interface Callback<T extends Loadable> {\n\n    /**\n     * Called when a load has completed.\n     *\n     * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting\n     * and this callback being called.\n     *\n     * @param loadable The loadable whose load has completed.\n     * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended.\n     * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}\n     *     was called.\n     */\n    void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs);\n\n    /**\n     * Called when a load has been canceled.\n     *\n     * <p>Note: If the {@link Loader} has not been released then there is guaranteed to be a memory\n     * barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link\n     * Loader} has been released then this callback may be called before {@link Loadable#load()}\n     * exits.\n     *\n     * @param loadable The loadable whose load has been canceled.\n     * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled.\n     * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}\n     *     was called up to the point at which it was canceled.\n     * @param released True if the load was canceled because the {@link Loader} was released. False\n     *     otherwise.\n     */\n    void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);\n\n    /**\n     * Called when a load encounters an error.\n     *\n     * <p>Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting\n     * and this callback being called.\n     *\n     * @param loadable The loadable whose load has encountered an error.\n     * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.\n     * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading}\n     *     was called up to the point at which the error occurred.\n     * @param error The load error.\n     * @param errorCount The number of errors this load has encountered, including this one.\n     * @return The desired error handling action. One of {@link Loader#RETRY}, {@link\n     *     Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link\n     *     Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}.\n     */\n    LoadErrorAction onLoadError(\n            T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount);\n  }\n\n  /**\n   * A callback to be notified when a {@link Loader} has finished being released.\n   */\n  public interface ReleaseCallback {\n\n    /**\n     * Called when the {@link Loader} has finished being released.\n     */\n    void onLoaderReleased();\n\n  }\n\n  /** Types of action that can be taken in response to a load error. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    ACTION_TYPE_RETRY,\n    ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT,\n    ACTION_TYPE_DONT_RETRY,\n    ACTION_TYPE_DONT_RETRY_FATAL\n  })\n  private @interface RetryActionType {}\n\n  private static final int ACTION_TYPE_RETRY = 0;\n  private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1;\n  private static final int ACTION_TYPE_DONT_RETRY = 2;\n  private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3;\n\n  /** Retries the load using the default delay. */\n  public static final LoadErrorAction RETRY =\n      createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET);\n  /** Retries the load using the default delay and resets the error count. */\n  public static final LoadErrorAction RETRY_RESET_ERROR_COUNT =\n      createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET);\n  /** Discards the failed {@link Loadable} and ignores any errors that have occurred. */\n  public static final LoadErrorAction DONT_RETRY =\n      new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET);\n  /**\n   * Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw\n   * the last load error.\n   */\n  public static final LoadErrorAction DONT_RETRY_FATAL =\n      new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET);\n\n  /**\n   * Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long,\n   * IOException, int)}.\n   */\n  public static final class LoadErrorAction {\n\n    private final @RetryActionType int type;\n    private final long retryDelayMillis;\n\n    private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) {\n      this.type = type;\n      this.retryDelayMillis = retryDelayMillis;\n    }\n\n    /** Returns whether this is a retry action. */\n    public boolean isRetry() {\n      return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT;\n    }\n  }\n\n  private final ExecutorService downloadExecutorService;\n\n  @Nullable private LoadTask<? extends Loadable> currentTask;\n  @Nullable private IOException fatalError;\n\n  /**\n   * @param threadName A name for the loader's thread.\n   */\n  public Loader(String threadName) {\n    this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);\n  }\n\n  /**\n   * Creates a {@link LoadErrorAction} for retrying with the given parameters.\n   *\n   * @param resetErrorCount Whether the previous error count should be set to zero.\n   * @param retryDelayMillis The number of milliseconds to wait before retrying.\n   * @return A {@link LoadErrorAction} for retrying with the given parameters.\n   */\n  public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) {\n    return new LoadErrorAction(\n        resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY,\n        retryDelayMillis);\n  }\n\n  /**\n   * Whether the last call to {@link #startLoading} resulted in a fatal error. Calling {@link\n   * #maybeThrowError()} will throw the fatal error.\n   */\n  public boolean hasFatalError() {\n    return fatalError != null;\n  }\n\n  /** Clears any stored fatal error. */\n  public void clearFatalError() {\n    fatalError = null;\n  }\n\n  /**\n   * Starts loading a {@link Loadable}.\n   *\n   * <p>The calling thread must be a {@link Looper} thread, which is the thread on which the {@link\n   * Callback} will be called.\n   *\n   * @param <T> The type of the loadable.\n   * @param loadable The {@link Loadable} to load.\n   * @param callback A callback to be called when the load ends.\n   * @param defaultMinRetryCount The minimum number of times the load must be retried before {@link\n   *     #maybeThrowError()} will propagate an error.\n   * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.\n   * @return {@link SystemClock#elapsedRealtime} when the load started.\n   */\n  public <T extends Loadable> long startLoading(\n      T loadable, Callback<T> callback, int defaultMinRetryCount) {\n    Looper looper = Assertions.checkStateNotNull(Looper.myLooper());\n    fatalError = null;\n    long startTimeMs = SystemClock.elapsedRealtime();\n    new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);\n    return startTimeMs;\n  }\n\n  /** Returns whether the loader is currently loading. */\n  public boolean isLoading() {\n    return currentTask != null;\n  }\n\n  /**\n   * Cancels the current load.\n   *\n   * @throws IllegalStateException If the loader is not currently loading.\n   */\n  public void cancelLoading() {\n    Assertions.checkStateNotNull(currentTask).cancel(false);\n  }\n\n  /** Releases the loader. This method should be called when the loader is no longer required. */\n  public void release() {\n    release(null);\n  }\n\n  /**\n   * Releases the loader. This method should be called when the loader is no longer required.\n   *\n   * @param callback An optional callback to be called on the loading thread once the loader has\n   *     been released.\n   */\n  public void release(@Nullable ReleaseCallback callback) {\n    if (currentTask != null) {\n      currentTask.cancel(true);\n    }\n    if (callback != null) {\n      downloadExecutorService.execute(new ReleaseTask(callback));\n    }\n    downloadExecutorService.shutdown();\n  }\n\n  // LoaderErrorThrower implementation.\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    maybeThrowError(Integer.MIN_VALUE);\n  }\n\n  @Override\n  public void maybeThrowError(int minRetryCount) throws IOException {\n    if (fatalError != null) {\n      throw fatalError;\n    } else if (currentTask != null) {\n      currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE\n          ? currentTask.defaultMinRetryCount : minRetryCount);\n    }\n  }\n\n  // Internal classes.\n\n  @SuppressLint(\"HandlerLeak\")\n  private final class LoadTask<T extends Loadable> extends Handler implements Runnable {\n\n    private static final String TAG = \"LoadTask\";\n\n    private static final int MSG_START = 0;\n    private static final int MSG_CANCEL = 1;\n    private static final int MSG_END_OF_SOURCE = 2;\n    private static final int MSG_IO_EXCEPTION = 3;\n    private static final int MSG_FATAL_ERROR = 4;\n\n    public final int defaultMinRetryCount;\n\n    private final T loadable;\n    private final long startTimeMs;\n\n    @Nullable private Loader.Callback<T> callback;\n    @Nullable private IOException currentError;\n    private int errorCount;\n\n    @Nullable private volatile Thread executorThread;\n    private volatile boolean canceled;\n    private volatile boolean released;\n\n    public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,\n        int defaultMinRetryCount, long startTimeMs) {\n      super(looper);\n      this.loadable = loadable;\n      this.callback = callback;\n      this.defaultMinRetryCount = defaultMinRetryCount;\n      this.startTimeMs = startTimeMs;\n    }\n\n    public void maybeThrowError(int minRetryCount) throws IOException {\n      if (currentError != null && errorCount > minRetryCount) {\n        throw currentError;\n      }\n    }\n\n    public void start(long delayMillis) {\n      Assertions.checkState(currentTask == null);\n      currentTask = this;\n      if (delayMillis > 0) {\n        sendEmptyMessageDelayed(MSG_START, delayMillis);\n      } else {\n        execute();\n      }\n    }\n\n    public void cancel(boolean released) {\n      this.released = released;\n      currentError = null;\n      if (hasMessages(MSG_START)) {\n        removeMessages(MSG_START);\n        if (!released) {\n          sendEmptyMessage(MSG_CANCEL);\n        }\n      } else {\n        canceled = true;\n        loadable.cancelLoad();\n        Thread executorThread = this.executorThread;\n        if (executorThread != null) {\n          executorThread.interrupt();\n        }\n      }\n      if (released) {\n        finish();\n        long nowMs = SystemClock.elapsedRealtime();\n        Assertions.checkNotNull(callback)\n            .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);\n        // If loading, this task will be referenced from a GC root (the loading thread) until\n        // cancellation completes. The time taken for cancellation to complete depends on the\n        // implementation of the Loadable that the task is loading. We null the callback reference\n        // here so that it doesn't prevent garbage collection whilst cancellation is ongoing.\n        callback = null;\n      }\n    }\n\n    @Override\n    public void run() {\n      try {\n        executorThread = Thread.currentThread();\n        if (!canceled) {\n          TraceUtil.beginSection(\"load:\" + loadable.getClass().getSimpleName());\n          try {\n            loadable.load();\n          } finally {\n            TraceUtil.endSection();\n          }\n        }\n        if (!released) {\n          sendEmptyMessage(MSG_END_OF_SOURCE);\n        }\n      } catch (IOException e) {\n        if (!released) {\n          obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();\n        }\n      } catch (InterruptedException e) {\n        // The load was canceled.\n        Assertions.checkState(canceled);\n        if (!released) {\n          sendEmptyMessage(MSG_END_OF_SOURCE);\n        }\n      } catch (Exception e) {\n        // This should never happen, but handle it anyway.\n        Log.e(TAG, \"Unexpected exception loading stream\", e);\n        if (!released) {\n          obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();\n        }\n      } catch (OutOfMemoryError e) {\n        // This can occur if a stream is malformed in a way that causes an extractor to think it\n        // needs to allocate a large amount of memory. We don't want the process to die in this\n        // case, but we do want the playback to fail.\n        Log.e(TAG, \"OutOfMemory error loading stream\", e);\n        if (!released) {\n          obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();\n        }\n      } catch (Error e) {\n        // We'd hope that the platform would kill the process if an Error is thrown here, but the\n        // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from\n        // the handler thread so that the process dies even if the executor behaves in this way.\n        Log.e(TAG, \"Unexpected error loading stream\", e);\n        if (!released) {\n          obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();\n        }\n        throw e;\n      }\n    }\n\n    @Override\n    public void handleMessage(Message msg) {\n      if (released) {\n        return;\n      }\n      if (msg.what == MSG_START) {\n        execute();\n        return;\n      }\n      if (msg.what == MSG_FATAL_ERROR) {\n        throw (Error) msg.obj;\n      }\n      finish();\n      long nowMs = SystemClock.elapsedRealtime();\n      long durationMs = nowMs - startTimeMs;\n      Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);\n      if (canceled) {\n        callback.onLoadCanceled(loadable, nowMs, durationMs, false);\n        return;\n      }\n      switch (msg.what) {\n        case MSG_CANCEL:\n          callback.onLoadCanceled(loadable, nowMs, durationMs, false);\n          break;\n        case MSG_END_OF_SOURCE:\n          try {\n            callback.onLoadCompleted(loadable, nowMs, durationMs);\n          } catch (RuntimeException e) {\n            // This should never happen, but handle it anyway.\n            Log.e(TAG, \"Unexpected exception handling load completed\", e);\n            fatalError = new UnexpectedLoaderException(e);\n          }\n          break;\n        case MSG_IO_EXCEPTION:\n          currentError = (IOException) msg.obj;\n          errorCount++;\n          LoadErrorAction action =\n              callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount);\n          if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) {\n            fatalError = currentError;\n          } else if (action.type != ACTION_TYPE_DONT_RETRY) {\n            if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) {\n              errorCount = 1;\n            }\n            start(\n                action.retryDelayMillis != C.TIME_UNSET\n                    ? action.retryDelayMillis\n                    : getRetryDelayMillis());\n          }\n          break;\n        default:\n          // Never happens.\n          break;\n      }\n    }\n\n    private void execute() {\n      currentError = null;\n      downloadExecutorService.execute(Assertions.checkNotNull(currentTask));\n    }\n\n    private void finish() {\n      currentTask = null;\n    }\n\n    private long getRetryDelayMillis() {\n      return Math.min((errorCount - 1) * 1000, 5000);\n    }\n\n  }\n\n  private static final class ReleaseTask implements Runnable {\n\n    private final ReleaseCallback callback;\n\n    public ReleaseTask(ReleaseCallback callback) {\n      this.callback = callback;\n    }\n\n    @Override\n    public void run() {\n      callback.onLoaderReleased();\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport java.io.IOException;\n\n/**\n * Conditionally throws errors affecting a {@link Loader}.\n */\npublic interface LoaderErrorThrower {\n\n  /**\n   * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current\n   * {@link Loadable} has incurred a number of errors greater than the {@link Loader}s default\n   * minimum number of retries. Else does nothing.\n   *\n   * @throws IOException The error.\n   */\n  void maybeThrowError() throws IOException;\n\n  /**\n   * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current\n   * {@link Loadable} has incurred a number of errors greater than the specified minimum number\n   * of retries. Else does nothing.\n   *\n   * @param minRetryCount A minimum retry count that must be exceeded for a non-fatal error to be\n   *     thrown. Should be non-negative.\n   * @throws IOException The error.\n   */\n  void maybeThrowError(int minRetryCount) throws IOException;\n\n  /**\n   * A {@link LoaderErrorThrower} that never throws.\n   */\n  final class Dummy implements LoaderErrorThrower {\n\n    @Override\n    public void maybeThrowError() throws IOException {\n      // Do nothing.\n    }\n\n    @Override\n    public void maybeThrowError(int minRetryCount) throws IOException {\n      // Do nothing.\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.upstream.Loader.Loadable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A {@link Loadable} for objects that can be parsed from binary data using a {@link Parser}.\n *\n * @param <T> The type of the object being loaded.\n */\npublic final class ParsingLoadable<T> implements Loadable {\n\n  /**\n   * Parses an object from loaded data.\n   */\n  public interface Parser<T> {\n\n    /**\n     * Parses an object from a response.\n     *\n     * @param uri The source {@link Uri} of the response, after any redirection.\n     * @param inputStream An {@link InputStream} from which the response data can be read.\n     * @return The parsed object.\n     * @throws ParserException If an error occurs parsing the data.\n     * @throws IOException If an error occurs reading data from the stream.\n     */\n    T parse(Uri uri, InputStream inputStream) throws IOException;\n\n  }\n\n  /**\n   * Loads a single parsable object.\n   *\n   * @param dataSource The {@link DataSource} through which the object should be read.\n   * @param parser The {@link Parser} to parse the object from the response.\n   * @param uri The {@link Uri} of the object to read.\n   * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants.\n   * @return The parsed object\n   * @throws IOException Thrown if there is an error while loading or parsing.\n   */\n  public static <T> T load(DataSource dataSource, Parser<? extends T> parser, Uri uri, int type)\n      throws IOException {\n    ParsingLoadable<T> loadable = new ParsingLoadable<>(dataSource, uri, type, parser);\n    loadable.load();\n    return Assertions.checkNotNull(loadable.getResult());\n  }\n\n  /**\n   * Loads a single parsable object.\n   *\n   * @param dataSource The {@link DataSource} through which the object should be read.\n   * @param parser The {@link Parser} to parse the object from the response.\n   * @param dataSpec The {@link DataSpec} of the object to read.\n   * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants.\n   * @return The parsed object\n   * @throws IOException Thrown if there is an error while loading or parsing.\n   */\n  public static <T> T load(\n      DataSource dataSource, Parser<? extends T> parser, DataSpec dataSpec, int type)\n      throws IOException {\n    ParsingLoadable<T> loadable = new ParsingLoadable<>(dataSource, dataSpec, type, parser);\n    loadable.load();\n    return Assertions.checkNotNull(loadable.getResult());\n  }\n\n  /**\n   * The {@link DataSpec} that defines the data to be loaded.\n   */\n  public final DataSpec dataSpec;\n  /**\n   * The type of the data. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For\n   * reporting only.\n   */\n  public final int type;\n\n  private final StatsDataSource dataSource;\n  private final Parser<? extends T> parser;\n\n  private volatile @Nullable T result;\n\n  /**\n   * @param dataSource A {@link DataSource} to use when loading the data.\n   * @param uri The {@link Uri} from which the object should be loaded.\n   * @param type See {@link #type}.\n   * @param parser Parses the object from the response.\n   */\n  public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser<? extends T> parser) {\n    this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser);\n  }\n\n  /**\n   * @param dataSource A {@link DataSource} to use when loading the data.\n   * @param dataSpec The {@link DataSpec} from which the object should be loaded.\n   * @param type See {@link #type}.\n   * @param parser Parses the object from the response.\n   */\n  public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type,\n      Parser<? extends T> parser) {\n    this.dataSource = new StatsDataSource(dataSource);\n    this.dataSpec = dataSpec;\n    this.type = type;\n    this.parser = parser;\n  }\n\n  /** Returns the loaded object, or null if an object has not been loaded. */\n  public final @Nullable T getResult() {\n    return result;\n  }\n\n  /**\n   * Returns the number of bytes loaded. In the case that the network response was compressed, the\n   * value returned is the size of the data <em>after</em> decompression. Must only be called after\n   * the load completed, failed, or was canceled.\n   */\n  public long bytesLoaded() {\n    return dataSource.getBytesRead();\n  }\n\n  /**\n   * Returns the {@link Uri} from which data was read. If redirection occurred, this is the\n   * redirected uri. Must only be called after the load completed, failed, or was canceled.\n   */\n  public Uri getUri() {\n    return dataSource.getLastOpenedUri();\n  }\n\n  /**\n   * Returns the response headers associated with the load. Must only be called after the load\n   * completed, failed, or was canceled.\n   */\n  public Map<String, List<String>> getResponseHeaders() {\n    return dataSource.getLastResponseHeaders();\n  }\n\n  @Override\n  public final void cancelLoad() {\n    // Do nothing.\n  }\n\n  @Override\n  public final void load() throws IOException {\n    // We always load from the beginning, so reset bytesRead to 0.\n    dataSource.resetBytesRead();\n    DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);\n    try {\n      inputStream.open();\n      Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri());\n      result = parser.parse(dataSourceUri, inputStream);\n    } finally {\n      Util.closeQuietly(inputStream);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A {@link DataSource} that can be used as part of a task registered with a\n * {@link PriorityTaskManager}.\n * <p>\n * Calls to {@link #open(DataSpec)} and {@link #read(byte[], int, int)} are allowed to proceed only\n * if there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there\n * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} is thrown.\n * <p>\n * Instances of this class are intended to be used as parts of (possibly larger) tasks that are\n * registered with the {@link PriorityTaskManager}, and hence do <em>not</em> register as tasks\n * themselves.\n */\npublic final class PriorityDataSource implements DataSource {\n\n  private final DataSource upstream;\n  private final PriorityTaskManager priorityTaskManager;\n  private final int priority;\n\n  /**\n   * @param upstream The upstream {@link DataSource}.\n   * @param priorityTaskManager The priority manager to which the task is registered.\n   * @param priority The priority of the task.\n   */\n  public PriorityDataSource(DataSource upstream, PriorityTaskManager priorityTaskManager,\n      int priority) {\n    this.upstream = Assertions.checkNotNull(upstream);\n    this.priorityTaskManager = Assertions.checkNotNull(priorityTaskManager);\n    this.priority = priority;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    upstream.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    priorityTaskManager.proceedOrThrow(priority);\n    return upstream.open(dataSpec);\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int max) throws IOException {\n    priorityTaskManager.proceedOrThrow(priority);\n    return upstream.read(buffer, offset, max);\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return upstream.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return upstream.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    upstream.close();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\n\n/**\n * A {@link Factory} that produces {@link PriorityDataSource} instances.\n */\npublic final class PriorityDataSourceFactory implements Factory {\n\n  private final Factory upstreamFactory;\n  private final PriorityTaskManager priorityTaskManager;\n  private final int priority;\n\n  /**\n   * @param upstreamFactory A {@link Factory} to be used to create an upstream {@link\n   *     DataSource} for {@link PriorityDataSource}.\n   * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered.\n   * @param priority The priority of PriorityDataSource task.\n   */\n  public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager,\n      int priority) {\n    this.upstreamFactory = upstreamFactory;\n    this.priorityTaskManager = priorityTaskManager;\n    this.priority = priority;\n  }\n\n  @Override\n  public PriorityDataSource createDataSource() {\n    return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager,\n        priority);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.content.Context;\nimport android.content.res.AssetFileDescriptor;\nimport android.content.res.Resources;\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.EOFException;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * A {@link DataSource} for reading a raw resource inside the APK.\n *\n * <p>URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where\n * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can\n * be used to build {@link Uri}s in this format.\n */\npublic final class RawResourceDataSource extends BaseDataSource {\n\n  /**\n   * Thrown when an {@link IOException} is encountered reading from a raw resource.\n   */\n  public static class RawResourceDataSourceException extends IOException {\n    public RawResourceDataSourceException(String message) {\n      super(message);\n    }\n\n    public RawResourceDataSourceException(IOException e) {\n      super(e);\n    }\n  }\n\n  /**\n   * Builds a {@link Uri} for the specified raw resource identifier.\n   *\n   * @param rawResourceId A raw resource identifier (i.e. a constant defined in {@code R.raw}).\n   * @return The corresponding {@link Uri}.\n   */\n  public static Uri buildRawResourceUri(int rawResourceId) {\n    return Uri.parse(RAW_RESOURCE_SCHEME + \":///\" + rawResourceId);\n  }\n\n  /** The scheme part of a raw resource URI. */\n  public static final String RAW_RESOURCE_SCHEME = \"rawresource\";\n\n  private final Resources resources;\n\n  @Nullable private Uri uri;\n  @Nullable private AssetFileDescriptor assetFileDescriptor;\n  @Nullable private InputStream inputStream;\n  private long bytesRemaining;\n  private boolean opened;\n\n  /**\n   * @param context A context.\n   */\n  public RawResourceDataSource(Context context) {\n    super(/* isNetwork= */ false);\n    this.resources = context.getResources();\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws RawResourceDataSourceException {\n    try {\n      Uri uri = dataSpec.uri;\n      this.uri = uri;\n      if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {\n        throw new RawResourceDataSourceException(\"URI must use scheme \" + RAW_RESOURCE_SCHEME);\n      }\n\n      int resourceId;\n      try {\n        resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment()));\n      } catch (NumberFormatException e) {\n        throw new RawResourceDataSourceException(\"Resource identifier must be an integer.\");\n      }\n\n      transferInitializing(dataSpec);\n      AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);\n      this.assetFileDescriptor = assetFileDescriptor;\n      if (assetFileDescriptor == null) {\n        throw new RawResourceDataSourceException(\"Resource is compressed: \" + uri);\n      }\n      FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());\n      this.inputStream = inputStream;\n\n      inputStream.skip(assetFileDescriptor.getStartOffset());\n      long skipped = inputStream.skip(dataSpec.position);\n      if (skipped < dataSpec.position) {\n        // We expect the skip to be satisfied in full. If it isn't then we're probably trying to\n        // skip beyond the end of the data.\n        throw new EOFException();\n      }\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        long assetFileDescriptorLength = assetFileDescriptor.getLength();\n        // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.\n        bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH\n            ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position);\n      }\n    } catch (IOException e) {\n      throw new RawResourceDataSourceException(e);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n\n    return bytesRemaining;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws RawResourceDataSourceException {\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    int bytesRead;\n    try {\n      int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength\n          : (int) Math.min(bytesRemaining, readLength);\n      bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);\n    } catch (IOException e) {\n      throw new RawResourceDataSourceException(e);\n    }\n\n    if (bytesRead == -1) {\n      if (bytesRemaining != C.LENGTH_UNSET) {\n        // End of stream reached having not read sufficient data.\n        throw new RawResourceDataSourceException(new EOFException());\n      }\n      return C.RESULT_END_OF_INPUT;\n    }\n    if (bytesRemaining != C.LENGTH_UNSET) {\n      bytesRemaining -= bytesRead;\n    }\n    bytesTransferred(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @SuppressWarnings(\"Finally\")\n  @Override\n  public void close() throws RawResourceDataSourceException {\n    uri = null;\n    try {\n      if (inputStream != null) {\n        inputStream.close();\n      }\n    } catch (IOException e) {\n      throw new RawResourceDataSourceException(e);\n    } finally {\n      inputStream = null;\n      try {\n        if (assetFileDescriptor != null) {\n          assetFileDescriptor.close();\n        }\n      } catch (IOException e) {\n        throw new RawResourceDataSourceException(e);\n      } finally {\n        assetFileDescriptor = null;\n        if (opened) {\n          opened = false;\n          transferEnded();\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\n/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */\npublic final class ResolvingDataSource implements DataSource {\n\n  /** Resolves {@link DataSpec DataSpecs}. */\n  public interface Resolver {\n\n    /**\n     * Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This\n     * method is allowed to block until the {@link DataSpec} has been resolved.\n     *\n     * <p>Note that this method is called for every new connection, so caching of results is\n     * recommended, especially if network operations are involved.\n     *\n     * @param dataSpec The original {@link DataSpec}.\n     * @return The resolved {@link DataSpec}.\n     * @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}.\n     */\n    DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException;\n\n    /**\n     * Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching\n     * purposes.\n     *\n     * <p>Implementations do not need to overwrite this method unless they want to change the\n     * reported URI.\n     *\n     * <p>This method is <em>not</em> allowed to block.\n     *\n     * @param uri The URI as reported by {@link DataSource#getUri()}.\n     * @return The resolved URI used for event reporting and caching.\n     */\n    default Uri resolveReportedUri(Uri uri) {\n      return uri;\n    }\n  }\n\n  /** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */\n  public static final class Factory implements DataSource.Factory {\n\n    private final DataSource.Factory upstreamFactory;\n    private final Resolver resolver;\n\n    /**\n     * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link\n     *     DataSpec DataSpecs}.\n     * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.\n     */\n    public Factory(DataSource.Factory upstreamFactory, Resolver resolver) {\n      this.upstreamFactory = upstreamFactory;\n      this.resolver = resolver;\n    }\n\n    @Override\n    public ResolvingDataSource createDataSource() {\n      return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver);\n    }\n  }\n\n  private final DataSource upstreamDataSource;\n  private final Resolver resolver;\n\n  private boolean upstreamOpened;\n\n  /**\n   * @param upstreamDataSource The wrapped {@link DataSource}.\n   * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.\n   */\n  public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) {\n    this.upstreamDataSource = upstreamDataSource;\n    this.resolver = resolver;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    upstreamDataSource.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec);\n    upstreamOpened = true;\n    return upstreamDataSource.open(resolvedDataSpec);\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    return upstreamDataSource.read(buffer, offset, readLength);\n  }\n\n  @Nullable\n  @Override\n  public Uri getUri() {\n    Uri reportedUri = upstreamDataSource.getUri();\n    return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri);\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return upstreamDataSource.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (upstreamOpened) {\n      upstreamOpened = false;\n      upstreamDataSource.close();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * {@link DataSource} wrapper which keeps track of bytes transferred, redirected uris, and response\n * headers.\n */\npublic final class StatsDataSource implements DataSource {\n\n  private final DataSource dataSource;\n\n  private long bytesRead;\n  private Uri lastOpenedUri;\n  private Map<String, List<String>> lastResponseHeaders;\n\n  /**\n   * Creates the stats data source.\n   *\n   * @param dataSource The wrapped {@link DataSource}.\n   */\n  public StatsDataSource(DataSource dataSource) {\n    this.dataSource = Assertions.checkNotNull(dataSource);\n    lastOpenedUri = Uri.EMPTY;\n    lastResponseHeaders = Collections.emptyMap();\n  }\n\n  /** Resets the number of bytes read as returned from {@link #getBytesRead()} to zero. */\n  public void resetBytesRead() {\n    bytesRead = 0;\n  }\n\n  /** Returns the total number of bytes that have been read from the data source. */\n  public long getBytesRead() {\n    return bytesRead;\n  }\n\n  /**\n   * Returns the {@link Uri} associated with the last {@link #open(DataSpec)} call. If redirection\n   * occurred, this is the redirected uri.\n   */\n  public Uri getLastOpenedUri() {\n    return lastOpenedUri;\n  }\n\n  /** Returns the response headers associated with the last {@link #open(DataSpec)} call. */\n  public Map<String, List<String>> getLastResponseHeaders() {\n    return lastResponseHeaders;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    dataSource.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    // Reassign defaults in case dataSource.open throws an exception.\n    lastOpenedUri = dataSpec.uri;\n    lastResponseHeaders = Collections.emptyMap();\n    long availableBytes = dataSource.open(dataSpec);\n    lastOpenedUri = Assertions.checkNotNull(getUri());\n    lastResponseHeaders = getResponseHeaders();\n    return availableBytes;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    int bytesRead = dataSource.read(buffer, offset, readLength);\n    if (bytesRead != C.RESULT_END_OF_INPUT) {\n      this.bytesRead += bytesRead;\n    }\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return dataSource.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return dataSource.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    dataSource.close();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Tees data into a {@link DataSink} as the data is read.\n */\npublic final class TeeDataSource implements DataSource {\n\n  private final DataSource upstream;\n  private final DataSink dataSink;\n\n  private boolean dataSinkNeedsClosing;\n  private long bytesRemaining;\n\n  /**\n   * @param upstream The upstream {@link DataSource}.\n   * @param dataSink The {@link DataSink} into which data is written.\n   */\n  public TeeDataSource(DataSource upstream, DataSink dataSink) {\n    this.upstream = Assertions.checkNotNull(upstream);\n    this.dataSink = Assertions.checkNotNull(dataSink);\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    upstream.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    bytesRemaining = upstream.open(dataSpec);\n    if (bytesRemaining == 0) {\n      return 0;\n    }\n    if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) {\n      // Reconstruct dataSpec in order to provide the resolved length to the sink.\n      dataSpec = dataSpec.subrange(0, bytesRemaining);\n    }\n    dataSinkNeedsClosing = true;\n    dataSink.open(dataSpec);\n    return bytesRemaining;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int max) throws IOException {\n    if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    int bytesRead = upstream.read(buffer, offset, max);\n    if (bytesRead > 0) {\n      // TODO: Consider continuing even if writes to the sink fail.\n      dataSink.write(buffer, offset, bytesRead);\n      if (bytesRemaining != C.LENGTH_UNSET) {\n        bytesRemaining -= bytesRead;\n      }\n    }\n    return bytesRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return upstream.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return upstream.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    try {\n      upstream.close();\n    } finally {\n      if (dataSinkNeedsClosing) {\n        dataSinkNeedsClosing = false;\n        dataSink.close();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\n/**\n * A listener of data transfer events.\n *\n * <p>A transfer usually progresses through multiple steps:\n *\n * <ol>\n *   <li>Initializing the underlying resource (e.g. opening a HTTP connection). {@link\n *       #onTransferInitializing(DataSource, DataSpec, boolean)} is called before the initialization\n *       starts.\n *   <li>Starting the transfer after successfully initializing the resource. {@link\n *       #onTransferStart(DataSource, DataSpec, boolean)} is called. Note that this only happens if\n *       the initialization was successful.\n *   <li>Transferring data. {@link #onBytesTransferred(DataSource, DataSpec, boolean, int)} is\n *       called frequently during the transfer to indicate progress.\n *   <li>Closing the transfer and the underlying resource. {@link #onTransferEnd(DataSource,\n *       DataSpec, boolean)} is called. Note that each {@link #onTransferStart(DataSource, DataSpec,\n *       boolean)} will have exactly one corresponding call to {@link #onTransferEnd(DataSource,\n *       DataSpec, boolean)}.\n * </ol>\n */\npublic interface TransferListener {\n\n  /**\n   * Called when a transfer is being initialized.\n   *\n   * @param source The source performing the transfer.\n   * @param dataSpec Describes the data for which the transfer is initialized.\n   * @param isNetwork Whether the data is transferred through a network.\n   */\n  void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork);\n\n  /**\n   * Called when a transfer starts.\n   *\n   * @param source The source performing the transfer.\n   * @param dataSpec Describes the data being transferred.\n   * @param isNetwork Whether the data is transferred through a network.\n   */\n  void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork);\n\n  /**\n   * Called incrementally during a transfer.\n   *\n   * @param source The source performing the transfer.\n   * @param dataSpec Describes the data being transferred.\n   * @param isNetwork Whether the data is transferred through a network.\n   * @param bytesTransferred The number of bytes transferred since the previous call to this method\n   */\n  void onBytesTransferred(\n          DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred);\n\n  /**\n   * Called when a transfer ends.\n   *\n   * @param source The source performing the transfer.\n   * @param dataSpec Describes the data being transferred.\n   * @param isNetwork Whether the data is transferred through a network.\n   */\n  void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\nimport java.net.DatagramPacket;\nimport java.net.DatagramSocket;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.MulticastSocket;\nimport java.net.SocketException;\n\n/** A UDP {@link DataSource}. */\npublic final class UdpDataSource extends BaseDataSource {\n\n  /**\n   * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}.\n   */\n  public static final class UdpDataSourceException extends IOException {\n\n    public UdpDataSourceException(IOException cause) {\n      super(cause);\n    }\n\n  }\n\n  /**\n   * The default maximum datagram packet size, in bytes.\n   */\n  public static final int DEFAULT_MAX_PACKET_SIZE = 2000;\n\n  /** The default socket timeout, in milliseconds. */\n  public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000;\n\n  private final int socketTimeoutMillis;\n  private final byte[] packetBuffer;\n  private final DatagramPacket packet;\n\n  @Nullable private Uri uri;\n  @Nullable private DatagramSocket socket;\n  @Nullable private MulticastSocket multicastSocket;\n  @Nullable private InetAddress address;\n  @Nullable private InetSocketAddress socketAddress;\n  private boolean opened;\n\n  private int packetRemaining;\n\n  public UdpDataSource() {\n    this(DEFAULT_MAX_PACKET_SIZE);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param maxPacketSize The maximum datagram packet size, in bytes.\n   */\n  public UdpDataSource(int maxPacketSize) {\n    this(maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param maxPacketSize The maximum datagram packet size, in bytes.\n   * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted\n   *     as an infinite timeout.\n   */\n  public UdpDataSource(int maxPacketSize, int socketTimeoutMillis) {\n    super(/* isNetwork= */ true);\n    this.socketTimeoutMillis = socketTimeoutMillis;\n    packetBuffer = new byte[maxPacketSize];\n    packet = new DatagramPacket(packetBuffer, 0, maxPacketSize);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws UdpDataSourceException {\n    uri = dataSpec.uri;\n    String host = uri.getHost();\n    int port = uri.getPort();\n    transferInitializing(dataSpec);\n    try {\n      address = InetAddress.getByName(host);\n      socketAddress = new InetSocketAddress(address, port);\n      if (address.isMulticastAddress()) {\n        multicastSocket = new MulticastSocket(socketAddress);\n        multicastSocket.joinGroup(address);\n        socket = multicastSocket;\n      } else {\n        socket = new DatagramSocket(socketAddress);\n      }\n    } catch (IOException e) {\n      throw new UdpDataSourceException(e);\n    }\n\n    try {\n      socket.setSoTimeout(socketTimeoutMillis);\n    } catch (SocketException e) {\n      throw new UdpDataSourceException(e);\n    }\n\n    opened = true;\n    transferStarted(dataSpec);\n    return C.LENGTH_UNSET;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException {\n    if (readLength == 0) {\n      return 0;\n    }\n\n    if (packetRemaining == 0) {\n      // We've read all of the data from the current packet. Get another.\n      try {\n        socket.receive(packet);\n      } catch (IOException e) {\n        throw new UdpDataSourceException(e);\n      }\n      packetRemaining = packet.getLength();\n      bytesTransferred(packetRemaining);\n    }\n\n    int packetOffset = packet.getLength() - packetRemaining;\n    int bytesToRead = Math.min(packetRemaining, readLength);\n    System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead);\n    packetRemaining -= bytesToRead;\n    return bytesToRead;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public void close() {\n    uri = null;\n    if (multicastSocket != null) {\n      try {\n        multicastSocket.leaveGroup(address);\n      } catch (IOException e) {\n        // Do nothing.\n      }\n      multicastSocket = null;\n    }\n    if (socket != null) {\n      socket.close();\n      socket = null;\n    }\n    address = null;\n    socketAddress = null;\n    packetRemaining = 0;\n    if (opened) {\n      opened = false;\n      transferEnded();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.C;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.NavigableSet;\nimport java.util.Set;\n\n/**\n * An interface for cache.\n */\npublic interface Cache {\n\n  /**\n   * Listener of {@link Cache} events.\n   */\n  interface Listener {\n\n    /**\n     * Called when a {@link CacheSpan} is added to the cache.\n     *\n     * @param cache The source of the event.\n     * @param span The added {@link CacheSpan}.\n     */\n    void onSpanAdded(Cache cache, CacheSpan span);\n\n    /**\n     * Called when a {@link CacheSpan} is removed from the cache.\n     *\n     * @param cache The source of the event.\n     * @param span The removed {@link CacheSpan}.\n     */\n    void onSpanRemoved(Cache cache, CacheSpan span);\n\n    /**\n     * Called when an existing {@link CacheSpan} is touched, causing it to be replaced. The new\n     * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however\n     * {@link CacheSpan#file} and {@link CacheSpan#lastTouchTimestamp} may have changed.\n     *\n     * <p>Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and {@link\n     * #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method.\n     *\n     * @param cache The source of the event.\n     * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache.\n     * @param newSpan The new {@link CacheSpan}, which has been added to the cache.\n     */\n    void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);\n  }\n\n  /**\n   * Thrown when an error is encountered when writing data.\n   */\n  class CacheException extends IOException {\n\n    public CacheException(String message) {\n      super(message);\n    }\n\n    public CacheException(Throwable cause) {\n      super(cause);\n    }\n\n    public CacheException(String message, Throwable cause) {\n      super(message, cause);\n    }\n  }\n\n  /**\n   * Returned by {@link #getUid()} if initialization failed before the unique identifier was read or\n   * generated.\n   */\n  long UID_UNSET = -1;\n\n  /**\n   * Returns a non-negative unique identifier for the cache, or {@link #UID_UNSET} if initialization\n   * failed before the unique identifier was determined.\n   *\n   * <p>Implementations are expected to generate and store the unique identifier alongside the\n   * cached content. If the location of the cache is deleted or swapped, it is expected that a new\n   * unique identifier will be generated when the cache is recreated.\n   */\n  long getUid();\n\n  /**\n   * Releases the cache. This method must be called when the cache is no longer required. The cache\n   * must not be used after calling this method.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   */\n  @WorkerThread\n  void release();\n\n  /**\n   * Registers a listener to listen for changes to a given key.\n   *\n   * <p>No guarantees are made about the thread or threads on which the listener is called, but it\n   * is guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and\n   * in the same order as events occurred.\n   *\n   * @param key The key to listen to.\n   * @param listener The listener to add.\n   * @return The current spans for the key.\n   */\n  NavigableSet<CacheSpan> addListener(String key, Listener listener);\n\n  /**\n   * Unregisters a listener.\n   *\n   * @param key The key to stop listening to.\n   * @param listener The listener to remove.\n   */\n  void removeListener(String key, Listener listener);\n\n  /**\n   * Returns the cached spans for a given cache key.\n   *\n   * @param key The key for which spans should be returned.\n   * @return The spans for the key.\n   */\n  NavigableSet<CacheSpan> getCachedSpans(String key);\n\n  /**\n   * Returns all keys in the cache.\n   *\n   * @return All the keys in the cache.\n   */\n  Set<String> getKeys();\n\n  /**\n   * Returns the total disk space in bytes used by the cache.\n   *\n   * @return The total disk space in bytes.\n   */\n  long getCacheSpace();\n\n  /**\n   * A caller should invoke this method when they require data from a given position for a given\n   * key.\n   *\n   * <p>If there is a cache entry that overlaps the position, then the returned {@link CacheSpan}\n   * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller\n   * may read from the cache file, but does not acquire any locks.\n   *\n   * <p>If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan}\n   * defines a hole in the cache starting at {@code position} into which the caller may write as it\n   * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock.\n   * Whilst the caller holds the lock it may write data into the hole. It may split data into\n   * multiple files. When the caller has finished writing a file it should commit it to the cache by\n   * calling {@link #commitFile(File, long)}. When the caller has finished writing, it must release\n   * the lock by calling {@link #releaseHoleSpan}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param key The key of the data being requested.\n   * @param position The position of the data being requested.\n   * @return The {@link CacheSpan}.\n   * @throws InterruptedException If the thread was interrupted.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;\n\n  /**\n   * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then\n   * instead of blocking, this method will return null as the {@link CacheSpan}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param key The key of the data being requested.\n   * @param position The position of the data being requested.\n   * @return The {@link CacheSpan}. Or null if the cache entry is locked.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  @Nullable\n  CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;\n\n  /**\n   * Obtains a cache file into which data can be written. Must only be called when holding a\n   * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param key The cache key for the data.\n   * @param position The starting position of the data.\n   * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. Used\n   *     only to ensure that there is enough space in the cache.\n   * @return The file into which data should be written.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  File startFile(String key, long position, long length) throws CacheException;\n\n  /**\n   * Commits a file into the cache. Must only be called when holding a corresponding hole {@link\n   * CacheSpan} obtained from {@link #startReadWrite(String, long)}.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param file A newly written cache file.\n   * @param length The length of the newly written cache file in bytes.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  void commitFile(File file, long length) throws CacheException;\n\n  /**\n   * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which\n   * corresponded to a hole in the cache.\n   *\n   * @param holeSpan The {@link CacheSpan} being released.\n   */\n  void releaseHoleSpan(CacheSpan holeSpan);\n\n  /**\n   * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param span The {@link CacheSpan} to remove.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  void removeSpan(CacheSpan span) throws CacheException;\n\n  /**\n   * Queries if a range is entirely available in the cache.\n   *\n   * @param key The cache key for the data.\n   * @param position The starting position of the data.\n   * @param length The length of the data.\n   * @return true if the data is available in the Cache otherwise false;\n   */\n  boolean isCached(String key, long position, long length);\n\n  /**\n   * Returns the length of the cached data block starting from the {@code position} to the block end\n   * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap\n   * to the next cached data up to {@code length} bytes) is returned.\n   *\n   * @param key The cache key for the data.\n   * @param position The starting position of the data.\n   * @param length The maximum length of the data to be returned.\n   * @return The length of the cached or not cached data block length.\n   */\n  long getCachedLength(String key, long position, long length);\n\n  /**\n   * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link\n   * CachedContent} is added if there isn't one already with the given key.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param key The cache key for the data.\n   * @param mutations Contains mutations to be applied to the metadata.\n   * @throws CacheException If an error is encountered.\n   */\n  @WorkerThread\n  void applyContentMetadataMutations(String key, ContentMetadataMutations mutations)\n      throws CacheException;\n\n  /**\n   * Returns a {@link ContentMetadata} for the given key.\n   *\n   * @param key The cache key for the data.\n   * @return A {@link ContentMetadata} for the given key.\n   */\n  ContentMetadata getContentMetadata(String key);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.cache.Cache.CacheException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.ReusableBufferedOutputStream;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * Writes data into a cache.\n *\n * <p>If the {@link DataSpec} passed to {@link #open(DataSpec)} has the {@code length} field set to\n * {@link C#LENGTH_UNSET} and {@link DataSpec#FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} set, then {@link\n * #write(byte[], int, int)} calls are ignored.\n */\npublic final class CacheDataSink implements DataSink {\n\n  /** Default {@code fragmentSize} recommended for caching use cases. */\n  public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024;\n  /** Default buffer size in bytes. */\n  public static final int DEFAULT_BUFFER_SIZE = 20 * 1024;\n\n  private static final long MIN_RECOMMENDED_FRAGMENT_SIZE = 2 * 1024 * 1024;\n  private static final String TAG = \"CacheDataSink\";\n\n  private final Cache cache;\n  private final long fragmentSize;\n  private final int bufferSize;\n\n  private DataSpec dataSpec;\n  private long dataSpecFragmentSize;\n  private File file;\n  private OutputStream outputStream;\n  private long outputStreamBytesWritten;\n  private long dataSpecBytesWritten;\n  private ReusableBufferedOutputStream bufferedOutputStream;\n\n  /**\n   * Thrown when IOException is encountered when writing data into sink.\n   */\n  public static class CacheDataSinkException extends CacheException {\n\n    public CacheDataSinkException(IOException cause) {\n      super(cause);\n    }\n\n  }\n\n  /**\n   * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}.\n   *\n   * @param cache The cache into which data should be written.\n   * @param fragmentSize For requests that should be fragmented into multiple cache files, this is\n   *     the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no\n   *     fragmentation will occur. Using a small value allows for finer-grained cache eviction\n   *     policies, at the cost of increased overhead both on the cache implementation and the file\n   *     system. Values under {@code (2 * 1024 * 1024)} are not recommended.\n   */\n  public CacheDataSink(Cache cache, long fragmentSize) {\n    this(cache, fragmentSize, DEFAULT_BUFFER_SIZE);\n  }\n\n  /**\n   * @param cache The cache into which data should be written.\n   * @param fragmentSize For requests that should be fragmented into multiple cache files, this is\n   *     the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no\n   *     fragmentation will occur. Using a small value allows for finer-grained cache eviction\n   *     policies, at the cost of increased overhead both on the cache implementation and the file\n   *     system. Values under {@code (2 * 1024 * 1024)} are not recommended.\n   * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative\n   *     value disables buffering.\n   */\n  public CacheDataSink(Cache cache, long fragmentSize, int bufferSize) {\n    Assertions.checkState(\n        fragmentSize > 0 || fragmentSize == C.LENGTH_UNSET,\n        \"fragmentSize must be positive or C.LENGTH_UNSET.\");\n    if (fragmentSize != C.LENGTH_UNSET && fragmentSize < MIN_RECOMMENDED_FRAGMENT_SIZE) {\n      Log.w(\n          TAG,\n          \"fragmentSize is below the minimum recommended value of \"\n              + MIN_RECOMMENDED_FRAGMENT_SIZE\n              + \". This may cause poor cache performance.\");\n    }\n    this.cache = Assertions.checkNotNull(cache);\n    this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize;\n    this.bufferSize = bufferSize;\n  }\n\n  @Override\n  public void open(DataSpec dataSpec) throws CacheDataSinkException {\n    if (dataSpec.length == C.LENGTH_UNSET\n        && dataSpec.isFlagSet(DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN)) {\n      this.dataSpec = null;\n      return;\n    }\n    this.dataSpec = dataSpec;\n    this.dataSpecFragmentSize =\n        dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) ? fragmentSize : Long.MAX_VALUE;\n    dataSpecBytesWritten = 0;\n    try {\n      openNextOutputStream();\n    } catch (IOException e) {\n      throw new CacheDataSinkException(e);\n    }\n  }\n\n  @Override\n  public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {\n    if (dataSpec == null) {\n      return;\n    }\n    try {\n      int bytesWritten = 0;\n      while (bytesWritten < length) {\n        if (outputStreamBytesWritten == dataSpecFragmentSize) {\n          closeCurrentOutputStream();\n          openNextOutputStream();\n        }\n        int bytesToWrite =\n            (int) Math.min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten);\n        outputStream.write(buffer, offset + bytesWritten, bytesToWrite);\n        bytesWritten += bytesToWrite;\n        outputStreamBytesWritten += bytesToWrite;\n        dataSpecBytesWritten += bytesToWrite;\n      }\n    } catch (IOException e) {\n      throw new CacheDataSinkException(e);\n    }\n  }\n\n  @Override\n  public void close() throws CacheDataSinkException {\n    if (dataSpec == null) {\n      return;\n    }\n    try {\n      closeCurrentOutputStream();\n    } catch (IOException e) {\n      throw new CacheDataSinkException(e);\n    }\n  }\n\n  private void openNextOutputStream() throws IOException {\n    long length =\n        dataSpec.length == C.LENGTH_UNSET\n            ? C.LENGTH_UNSET\n            : Math.min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize);\n    file =\n        cache.startFile(\n            dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length);\n    FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);\n    if (bufferSize > 0) {\n      if (bufferedOutputStream == null) {\n        bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream,\n            bufferSize);\n      } else {\n        bufferedOutputStream.reset(underlyingFileOutputStream);\n      }\n      outputStream = bufferedOutputStream;\n    } else {\n      outputStream = underlyingFileOutputStream;\n    }\n    outputStreamBytesWritten = 0;\n  }\n\n  private void closeCurrentOutputStream() throws IOException {\n    if (outputStream == null) {\n      return;\n    }\n\n    boolean success = false;\n    try {\n      outputStream.flush();\n      success = true;\n    } finally {\n      Util.closeQuietly(outputStream);\n      outputStream = null;\n      File fileToCommit = file;\n      file = null;\n      if (success) {\n        cache.commitFile(fileToCommit, outputStreamBytesWritten);\n      } else {\n        fileToCommit.delete();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport com.google.android.exoplayer2.upstream.DataSink;\n\n/**\n * A {@link DataSink.Factory} that produces {@link CacheDataSink}.\n */\npublic final class CacheDataSinkFactory implements DataSink.Factory {\n\n  private final Cache cache;\n  private final long fragmentSize;\n  private final int bufferSize;\n\n  /** @see CacheDataSink#CacheDataSink(Cache, long) */\n  public CacheDataSinkFactory(Cache cache, long fragmentSize) {\n    this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE);\n  }\n\n  /** @see CacheDataSink#CacheDataSink(Cache, long, int) */\n  public CacheDataSinkFactory(Cache cache, long fragmentSize, int bufferSize) {\n    this.cache = cache;\n    this.fragmentSize = fragmentSize;\n    this.bufferSize = bufferSize;\n  }\n\n  @Override\n  public DataSink createDataSink() {\n    return new CacheDataSink(cache, fragmentSize, bufferSize);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.net.Uri;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.upstream.TeeDataSource;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.upstream.cache.Cache.CacheException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache\n * when possible. When data is not cached it is requested from an upstream {@link DataSource} and\n * written into the cache.\n */\npublic final class CacheDataSource implements DataSource {\n\n  /**\n   * Flags controlling the CacheDataSource's behavior. Possible flag values are {@link\n   * #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link\n   * #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        FLAG_BLOCK_ON_CACHE,\n        FLAG_IGNORE_CACHE_ON_ERROR,\n        FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS\n      })\n  public @interface Flags {}\n  /**\n   * A flag indicating whether we will block reads if the cache key is locked. If unset then data is\n   * read from upstream if the cache key is locked, regardless of whether the data is cached.\n   */\n  public static final int FLAG_BLOCK_ON_CACHE = 1;\n\n  /**\n   * A flag indicating whether the cache is bypassed following any cache related error. If set\n   * then cache related exceptions may be thrown for one cycle of open, read and close calls.\n   * Subsequent cycles of these calls will then bypass the cache.\n   */\n  public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; // 2\n\n  /**\n   * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This\n   * flag is provided for legacy reasons only.\n   */\n  public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4\n\n  /**\n   * Reasons the cache may be ignored. One of {@link #CACHE_IGNORED_REASON_ERROR} or {@link\n   * #CACHE_IGNORED_REASON_UNSET_LENGTH}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH})\n  public @interface CacheIgnoredReason {}\n\n  /** Cache not ignored. */\n  private static final int CACHE_NOT_IGNORED = -1;\n\n  /** Cache ignored due to a cache related error. */\n  public static final int CACHE_IGNORED_REASON_ERROR = 0;\n\n  /** Cache ignored due to a request with an unset length. */\n  public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1;\n\n  /**\n   * Listener of {@link CacheDataSource} events.\n   */\n  public interface EventListener {\n\n    /**\n     * Called when bytes have been read from the cache.\n     *\n     * @param cacheSizeBytes Current cache size in bytes.\n     * @param cachedBytesRead Total bytes read from the cache since this method was last called.\n     */\n    void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);\n\n    /**\n     * Called when the current request ignores cache.\n     *\n     * @param reason Reason cache is bypassed.\n     */\n    void onCacheIgnored(@CacheIgnoredReason int reason);\n  }\n\n  /** Minimum number of bytes to read before checking cache for availability. */\n  private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024;\n\n  private final Cache cache;\n  private final DataSource cacheReadDataSource;\n  @Nullable private final DataSource cacheWriteDataSource;\n  private final DataSource upstreamDataSource;\n  private final CacheKeyFactory cacheKeyFactory;\n  @Nullable private final EventListener eventListener;\n\n  private final boolean blockOnCache;\n  private final boolean ignoreCacheOnError;\n  private final boolean ignoreCacheForUnsetLengthRequests;\n\n  @Nullable private DataSource currentDataSource;\n  private boolean currentDataSpecLengthUnset;\n  @Nullable private Uri uri;\n  @Nullable private Uri actualUri;\n  @HttpMethod private int httpMethod;\n  @Nullable private byte[] httpBody;\n  private Map<String, String> httpRequestHeaders = Collections.emptyMap();\n  @DataSpec.Flags private int flags;\n  @Nullable private String key;\n  private long readPosition;\n  private long bytesRemaining;\n  @Nullable private CacheSpan currentHoleSpan;\n  private boolean seenCacheError;\n  private boolean currentRequestIgnoresCache;\n  private long totalCachedBytesRead;\n  private long checkCachePosition;\n\n  /**\n   * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for\n   * reading and writing the cache.\n   *\n   * @param cache The cache.\n   * @param upstream A {@link DataSource} for reading data not in the cache.\n   */\n  public CacheDataSource(Cache cache, DataSource upstream) {\n    this(cache, upstream, /* flags= */ 0);\n  }\n\n  /**\n   * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for\n   * reading and writing the cache.\n   *\n   * @param cache The cache.\n   * @param upstream A {@link DataSource} for reading data not in the cache.\n   * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}\n   *     and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.\n   */\n  public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {\n    this(\n        cache,\n        upstream,\n        new FileDataSource(),\n        new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),\n        flags,\n        /* eventListener= */ null);\n  }\n\n  /**\n   * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for\n   * reading and writing the cache. One use of this constructor is to allow data to be transformed\n   * before it is written to disk.\n   *\n   * @param cache The cache.\n   * @param upstream A {@link DataSource} for reading data not in the cache.\n   * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.\n   * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is\n   *     accessed read-only.\n   * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}\n   *     and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.\n   * @param eventListener An optional {@link EventListener} to receive events.\n   */\n  public CacheDataSource(\n      Cache cache,\n      DataSource upstream,\n      DataSource cacheReadDataSource,\n      @Nullable DataSink cacheWriteDataSink,\n      @Flags int flags,\n      @Nullable EventListener eventListener) {\n    this(\n        cache,\n        upstream,\n        cacheReadDataSource,\n        cacheWriteDataSink,\n        flags,\n        eventListener,\n        /* cacheKeyFactory= */ null);\n  }\n\n  /**\n   * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for\n   * reading and writing the cache. One use of this constructor is to allow data to be transformed\n   * before it is written to disk.\n   *\n   * @param cache The cache.\n   * @param upstream A {@link DataSource} for reading data not in the cache.\n   * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.\n   * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is\n   *     accessed read-only.\n   * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}\n   *     and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.\n   * @param eventListener An optional {@link EventListener} to receive events.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   */\n  public CacheDataSource(\n      Cache cache,\n      DataSource upstream,\n      DataSource cacheReadDataSource,\n      @Nullable DataSink cacheWriteDataSink,\n      @Flags int flags,\n      @Nullable EventListener eventListener,\n      @Nullable CacheKeyFactory cacheKeyFactory) {\n    this.cache = cache;\n    this.cacheReadDataSource = cacheReadDataSource;\n    this.cacheKeyFactory =\n        cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY;\n    this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0;\n    this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;\n    this.ignoreCacheForUnsetLengthRequests =\n        (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;\n    this.upstreamDataSource = upstream;\n    if (cacheWriteDataSink != null) {\n      this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);\n    } else {\n      this.cacheWriteDataSource = null;\n    }\n    this.eventListener = eventListener;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    cacheReadDataSource.addTransferListener(transferListener);\n    upstreamDataSource.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    try {\n      key = cacheKeyFactory.buildCacheKey(dataSpec);\n      uri = dataSpec.uri;\n      actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);\n      httpMethod = dataSpec.httpMethod;\n      httpBody = dataSpec.httpBody;\n      httpRequestHeaders = dataSpec.httpRequestHeaders;\n      flags = dataSpec.flags;\n      readPosition = dataSpec.position;\n\n      int reason = shouldIgnoreCacheForRequest(dataSpec);\n      currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED;\n      if (currentRequestIgnoresCache) {\n        notifyCacheIgnored(reason);\n      }\n\n      if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));\n        if (bytesRemaining != C.LENGTH_UNSET) {\n          bytesRemaining -= dataSpec.position;\n          if (bytesRemaining <= 0) {\n            throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);\n          }\n        }\n      }\n      openNextSource(false);\n      return bytesRemaining;\n    } catch (Throwable e) {\n      handleBeforeThrow(e);\n      throw e;\n    }\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    if (readLength == 0) {\n      return 0;\n    }\n    if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    try {\n      if (readPosition >= checkCachePosition) {\n        openNextSource(true);\n      }\n      int bytesRead = currentDataSource.read(buffer, offset, readLength);\n      if (bytesRead != C.RESULT_END_OF_INPUT) {\n        if (isReadingFromCache()) {\n          totalCachedBytesRead += bytesRead;\n        }\n        readPosition += bytesRead;\n        if (bytesRemaining != C.LENGTH_UNSET) {\n          bytesRemaining -= bytesRead;\n        }\n      } else if (currentDataSpecLengthUnset) {\n        setNoBytesRemainingAndMaybeStoreLength();\n      } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {\n        closeCurrentSource();\n        openNextSource(false);\n        return read(buffer, offset, readLength);\n      }\n      return bytesRead;\n    } catch (IOException e) {\n      if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) {\n        setNoBytesRemainingAndMaybeStoreLength();\n        return C.RESULT_END_OF_INPUT;\n      }\n      handleBeforeThrow(e);\n      throw e;\n    } catch (Throwable e) {\n      handleBeforeThrow(e);\n      throw e;\n    }\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return actualUri;\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    // TODO: Implement.\n    return isReadingFromUpstream()\n        ? upstreamDataSource.getResponseHeaders()\n        : Collections.emptyMap();\n  }\n\n  @Override\n  public void close() throws IOException {\n    uri = null;\n    actualUri = null;\n    httpMethod = DataSpec.HTTP_METHOD_GET;\n    httpBody = null;\n    httpRequestHeaders = Collections.emptyMap();\n    flags = 0;\n    readPosition = 0;\n    key = null;\n    notifyBytesRead();\n    try {\n      closeCurrentSource();\n    } catch (Throwable e) {\n      handleBeforeThrow(e);\n      throw e;\n    }\n  }\n\n  /**\n   * Opens the next source. If the cache contains data spanning the current read position then\n   * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is\n   * opened to read from the upstream source and write into the cache.\n   *\n   * <p>There must not be a currently open source when this method is called, except in the case\n   * that {@code checkCache} is true. If {@code checkCache} is true then there must be a currently\n   * open source, and it must be {@link #upstreamDataSource}. It will be closed and a new source\n   * opened if it's possible to switch to reading from or writing to the cache. If a switch isn't\n   * possible then the current source is left unchanged.\n   *\n   * @param checkCache If true tries to switch to reading from or writing to cache instead of\n   *     reading from {@link #upstreamDataSource}, which is the currently open source.\n   */\n  private void openNextSource(boolean checkCache) throws IOException {\n    CacheSpan nextSpan;\n    if (currentRequestIgnoresCache) {\n      nextSpan = null;\n    } else if (blockOnCache) {\n      try {\n        nextSpan = cache.startReadWrite(key, readPosition);\n      } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n        throw new InterruptedIOException();\n      }\n    } else {\n      nextSpan = cache.startReadWriteNonBlocking(key, readPosition);\n    }\n\n    DataSpec nextDataSpec;\n    DataSource nextDataSource;\n    if (nextSpan == null) {\n      // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read\n      // from upstream.\n      nextDataSource = upstreamDataSource;\n      nextDataSpec =\n          new DataSpec(\n              uri,\n              httpMethod,\n              httpBody,\n              readPosition,\n              readPosition,\n              bytesRemaining,\n              key,\n              flags,\n              httpRequestHeaders);\n    } else if (nextSpan.isCached) {\n      // Data is cached, read from cache.\n      Uri fileUri = Uri.fromFile(nextSpan.file);\n      long filePosition = readPosition - nextSpan.position;\n      long length = nextSpan.length - filePosition;\n      if (bytesRemaining != C.LENGTH_UNSET) {\n        length = Math.min(length, bytesRemaining);\n      }\n      // Deliberately skip the HTTP-related parameters since we're reading from the cache, not\n      // making an HTTP request.\n      nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags);\n      nextDataSource = cacheReadDataSource;\n    } else {\n      // Data is not cached, and data is not locked, read from upstream with cache backing.\n      long length;\n      if (nextSpan.isOpenEnded()) {\n        length = bytesRemaining;\n      } else {\n        length = nextSpan.length;\n        if (bytesRemaining != C.LENGTH_UNSET) {\n          length = Math.min(length, bytesRemaining);\n        }\n      }\n      nextDataSpec =\n          new DataSpec(\n              uri,\n              httpMethod,\n              httpBody,\n              readPosition,\n              readPosition,\n              length,\n              key,\n              flags,\n              httpRequestHeaders);\n      if (cacheWriteDataSource != null) {\n        nextDataSource = cacheWriteDataSource;\n      } else {\n        nextDataSource = upstreamDataSource;\n        cache.releaseHoleSpan(nextSpan);\n        nextSpan = null;\n      }\n    }\n\n    checkCachePosition =\n        !currentRequestIgnoresCache && nextDataSource == upstreamDataSource\n            ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE\n            : Long.MAX_VALUE;\n    if (checkCache) {\n      Assertions.checkState(isBypassingCache());\n      if (nextDataSource == upstreamDataSource) {\n        // Continue reading from upstream.\n        return;\n      }\n      // We're switching to reading from or writing to the cache.\n      try {\n        closeCurrentSource();\n      } catch (Throwable e) {\n        if (nextSpan.isHoleSpan()) {\n          // Release the hole span before throwing, else we'll hold it forever.\n          cache.releaseHoleSpan(nextSpan);\n        }\n        throw e;\n      }\n    }\n\n    if (nextSpan != null && nextSpan.isHoleSpan()) {\n      currentHoleSpan = nextSpan;\n    }\n    currentDataSource = nextDataSource;\n    currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;\n    long resolvedLength = nextDataSource.open(nextDataSpec);\n\n    // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {\n      bytesRemaining = resolvedLength;\n      ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);\n    }\n    if (isReadingFromUpstream()) {\n      actualUri = currentDataSource.getUri();\n      boolean isRedirected = !uri.equals(actualUri);\n      ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);\n    }\n    if (isWritingToCache()) {\n      cache.applyContentMetadataMutations(key, mutations);\n    }\n  }\n\n  private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {\n    bytesRemaining = 0;\n    if (isWritingToCache()) {\n      ContentMetadataMutations mutations = new ContentMetadataMutations();\n      ContentMetadataMutations.setContentLength(mutations, readPosition);\n      cache.applyContentMetadataMutations(key, mutations);\n    }\n  }\n\n  private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {\n    Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));\n    return redirectedUri != null ? redirectedUri : defaultUri;\n  }\n\n  private boolean isReadingFromUpstream() {\n    return !isReadingFromCache();\n  }\n\n  private boolean isBypassingCache() {\n    return currentDataSource == upstreamDataSource;\n  }\n\n  private boolean isReadingFromCache() {\n    return currentDataSource == cacheReadDataSource;\n  }\n\n  private boolean isWritingToCache() {\n    return currentDataSource == cacheWriteDataSource;\n  }\n\n  private void closeCurrentSource() throws IOException {\n    if (currentDataSource == null) {\n      return;\n    }\n    try {\n      currentDataSource.close();\n    } finally {\n      currentDataSource = null;\n      currentDataSpecLengthUnset = false;\n      if (currentHoleSpan != null) {\n        cache.releaseHoleSpan(currentHoleSpan);\n        currentHoleSpan = null;\n      }\n    }\n  }\n\n  private void handleBeforeThrow(Throwable exception) {\n    if (isReadingFromCache() || exception instanceof CacheException) {\n      seenCacheError = true;\n    }\n  }\n\n  private int shouldIgnoreCacheForRequest(DataSpec dataSpec) {\n    if (ignoreCacheOnError && seenCacheError) {\n      return CACHE_IGNORED_REASON_ERROR;\n    } else if (ignoreCacheForUnsetLengthRequests && dataSpec.length == C.LENGTH_UNSET) {\n      return CACHE_IGNORED_REASON_UNSET_LENGTH;\n    } else {\n      return CACHE_NOT_IGNORED;\n    }\n  }\n\n  private void notifyCacheIgnored(@CacheIgnoredReason int reason) {\n    if (eventListener != null) {\n      eventListener.onCacheIgnored(reason);\n    }\n  }\n\n  private void notifyBytesRead() {\n    if (eventListener != null && totalCachedBytesRead > 0) {\n      eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);\n      totalCachedBytesRead = 0;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\n\n/** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */\npublic final class CacheDataSourceFactory implements DataSource.Factory {\n\n  private final Cache cache;\n  private final DataSource.Factory upstreamFactory;\n  private final DataSource.Factory cacheReadDataSourceFactory;\n  @CacheDataSource.Flags private final int flags;\n  @Nullable private final DataSink.Factory cacheWriteDataSinkFactory;\n  @Nullable private final CacheDataSource.EventListener eventListener;\n  @Nullable private final CacheKeyFactory cacheKeyFactory;\n\n  /**\n   * Constructs a factory which creates {@link CacheDataSource} instances with default {@link\n   * DataSource} and {@link DataSink} instances for reading and writing the cache.\n   *\n   * @param cache The cache.\n   * @param upstreamFactory A {@link DataSource.Factory} for creating upstream {@link DataSource}s\n   *     for reading data not in the cache.\n   */\n  public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) {\n    this(cache, upstreamFactory, /* flags= */ 0);\n  }\n\n  /** @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */\n  public CacheDataSourceFactory(\n      Cache cache, DataSource.Factory upstreamFactory, @CacheDataSource.Flags int flags) {\n    this(\n        cache,\n        upstreamFactory,\n        new FileDataSource.Factory(),\n        new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),\n        flags,\n        /* eventListener= */ null);\n  }\n\n  /**\n   * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,\n   *     CacheDataSource.EventListener)\n   */\n  public CacheDataSourceFactory(\n      Cache cache,\n      DataSource.Factory upstreamFactory,\n      DataSource.Factory cacheReadDataSourceFactory,\n      @Nullable DataSink.Factory cacheWriteDataSinkFactory,\n      @CacheDataSource.Flags int flags,\n      @Nullable CacheDataSource.EventListener eventListener) {\n    this(\n        cache,\n        upstreamFactory,\n        cacheReadDataSourceFactory,\n        cacheWriteDataSinkFactory,\n        flags,\n        eventListener,\n        /* cacheKeyFactory= */ null);\n  }\n\n  /**\n   * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,\n   *     CacheDataSource.EventListener, CacheKeyFactory)\n   */\n  public CacheDataSourceFactory(\n      Cache cache,\n      DataSource.Factory upstreamFactory,\n      DataSource.Factory cacheReadDataSourceFactory,\n      @Nullable DataSink.Factory cacheWriteDataSinkFactory,\n      @CacheDataSource.Flags int flags,\n      @Nullable CacheDataSource.EventListener eventListener,\n      @Nullable CacheKeyFactory cacheKeyFactory) {\n    this.cache = cache;\n    this.upstreamFactory = upstreamFactory;\n    this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;\n    this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;\n    this.flags = flags;\n    this.eventListener = eventListener;\n    this.cacheKeyFactory = cacheKeyFactory;\n  }\n\n  @Override\n  public CacheDataSource createDataSource() {\n    return new CacheDataSource(\n        cache,\n        upstreamFactory.createDataSource(),\n        cacheReadDataSourceFactory.createDataSource(),\n        cacheWriteDataSinkFactory == null ? null : cacheWriteDataSinkFactory.createDataSink(),\n        flags,\n        eventListener,\n        cacheKeyFactory);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport com.google.android.exoplayer2.C;\n\n/**\n * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)}\n * to evict cache entries based on their eviction policies.\n */\npublic interface CacheEvictor extends Cache.Listener {\n\n  /**\n   * Returns whether the evictor requires the {@link Cache} to touch {@link CacheSpan CacheSpans}\n   * when it accesses them. Implementations that do not use {@link CacheSpan#lastTouchTimestamp}\n   * should return {@code false}.\n   */\n  boolean requiresCacheSpanTouches();\n\n  /**\n   * Called when cache has been initialized.\n   */\n  void onCacheInitialized();\n\n  /**\n   * Called when a writer starts writing to the cache.\n   *\n   * @param cache The source of the event.\n   * @param key The key being written.\n   * @param position The starting position of the data being written.\n   * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown.\n   */\n  void onStartFile(Cache cache, String key, long position, long length);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\n/** Metadata associated with a cache file. */\n/* package */ final class CacheFileMetadata {\n\n  public final long length;\n  public final long lastTouchTimestamp;\n\n  public CacheFileMetadata(long length, long lastTouchTimestamp) {\n    this.length = length;\n    this.lastTouchTimestamp = lastTouchTimestamp;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.database.DatabaseIOException;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.database.VersionTable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** Maintains an index of cache file metadata. */\n/* package */ final class CacheFileMetadataIndex {\n\n  private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + \"CacheFileMetadata\";\n  private static final int TABLE_VERSION = 1;\n\n  private static final String COLUMN_NAME = \"name\";\n  private static final String COLUMN_LENGTH = \"length\";\n  private static final String COLUMN_LAST_TOUCH_TIMESTAMP = \"last_touch_timestamp\";\n\n  private static final int COLUMN_INDEX_NAME = 0;\n  private static final int COLUMN_INDEX_LENGTH = 1;\n  private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2;\n\n  private static final String WHERE_NAME_EQUALS = COLUMN_NAME + \" = ?\";\n\n  private static final String[] COLUMNS =\n      new String[] {\n        COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP,\n      };\n  private static final String TABLE_SCHEMA =\n      \"(\"\n          + COLUMN_NAME\n          + \" TEXT PRIMARY KEY NOT NULL,\"\n          + COLUMN_LENGTH\n          + \" INTEGER NOT NULL,\"\n          + COLUMN_LAST_TOUCH_TIMESTAMP\n          + \" INTEGER NOT NULL)\";\n\n  private final DatabaseProvider databaseProvider;\n\n  private @MonotonicNonNull String tableName;\n\n  /**\n   * Deletes index data for the specified cache.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param databaseProvider Provides the database in which the index is stored.\n   * @param uid The cache UID.\n   * @throws DatabaseIOException If an error occurs deleting the index data.\n   */\n  @WorkerThread\n  public static void delete(DatabaseProvider databaseProvider, long uid)\n      throws DatabaseIOException {\n    String hexUid = Long.toHexString(uid);\n    try {\n      String tableName = getTableName(hexUid);\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.beginTransactionNonExclusive();\n      try {\n        VersionTable.removeVersion(\n            writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);\n        dropTable(writableDatabase, tableName);\n        writableDatabase.setTransactionSuccessful();\n      } finally {\n        writableDatabase.endTransaction();\n      }\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /** @param databaseProvider Provides the database in which the index is stored. */\n  public CacheFileMetadataIndex(DatabaseProvider databaseProvider) {\n    this.databaseProvider = databaseProvider;\n  }\n\n  /**\n   * Initializes the index for the given cache UID.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param uid The cache UID.\n   * @throws DatabaseIOException If an error occurs initializing the index.\n   */\n  @WorkerThread\n  public void initialize(long uid) throws DatabaseIOException {\n    try {\n      String hexUid = Long.toHexString(uid);\n      tableName = getTableName(hexUid);\n      SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();\n      int version =\n          VersionTable.getVersion(\n              readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);\n      if (version != TABLE_VERSION) {\n        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n        writableDatabase.beginTransactionNonExclusive();\n        try {\n          VersionTable.setVersion(\n              writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION);\n          dropTable(writableDatabase, tableName);\n          writableDatabase.execSQL(\"CREATE TABLE \" + tableName + \" \" + TABLE_SCHEMA);\n          writableDatabase.setTransactionSuccessful();\n        } finally {\n          writableDatabase.endTransaction();\n        }\n      }\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Returns all file metadata keyed by file name. The returned map is mutable and may be modified\n   * by the caller.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @return The file metadata keyed by file name.\n   * @throws DatabaseIOException If an error occurs loading the metadata.\n   */\n  @WorkerThread\n  public Map<String, CacheFileMetadata> getAll() throws DatabaseIOException {\n    try (Cursor cursor = getCursor()) {\n      Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount());\n      while (cursor.moveToNext()) {\n        String name = cursor.getString(COLUMN_INDEX_NAME);\n        long length = cursor.getLong(COLUMN_INDEX_LENGTH);\n        long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP);\n        fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp));\n      }\n      return fileMetadata;\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Sets metadata for a given file.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param name The name of the file.\n   * @param length The file length.\n   * @param lastTouchTimestamp The file last touch timestamp.\n   * @throws DatabaseIOException If an error occurs setting the metadata.\n   */\n  @WorkerThread\n  public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException {\n    Assertions.checkNotNull(tableName);\n    try {\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_NAME, name);\n      values.put(COLUMN_LENGTH, length);\n      values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp);\n      writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Removes metadata.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param name The name of the file whose metadata is to be removed.\n   * @throws DatabaseIOException If an error occurs removing the metadata.\n   */\n  @WorkerThread\n  public void remove(String name) throws DatabaseIOException {\n    Assertions.checkNotNull(tableName);\n    try {\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  /**\n   * Removes metadata.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param names The names of the files whose metadata is to be removed.\n   * @throws DatabaseIOException If an error occurs removing the metadata.\n   */\n  @WorkerThread\n  public void removeAll(Set<String> names) throws DatabaseIOException {\n    Assertions.checkNotNull(tableName);\n    try {\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.beginTransactionNonExclusive();\n      try {\n        for (String name : names) {\n          writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});\n        }\n        writableDatabase.setTransactionSuccessful();\n      } finally {\n        writableDatabase.endTransaction();\n      }\n    } catch (SQLException e) {\n      throw new DatabaseIOException(e);\n    }\n  }\n\n  private Cursor getCursor() {\n    Assertions.checkNotNull(tableName);\n    return databaseProvider\n        .getReadableDatabase()\n        .query(\n            tableName,\n            COLUMNS,\n            /* selection */ null,\n            /* selectionArgs= */ null,\n            /* groupBy= */ null,\n            /* having= */ null,\n            /* orderBy= */ null);\n  }\n\n  private static void dropTable(SQLiteDatabase writableDatabase, String tableName) {\n    writableDatabase.execSQL(\"DROP TABLE IF EXISTS \" + tableName);\n  }\n\n  private static String getTableName(String hexUid) {\n    return TABLE_PREFIX + hexUid;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport com.google.android.exoplayer2.upstream.DataSpec;\n\n/** Factory for cache keys. */\npublic interface CacheKeyFactory {\n\n  /**\n   * Returns a cache key for the given {@link DataSpec}.\n   *\n   * @param dataSpec The data being cached.\n   */\n  String buildCacheKey(DataSpec dataSpec);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.File;\n\n/**\n * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).\n */\npublic class CacheSpan implements Comparable<CacheSpan> {\n\n  /**\n   * The cache key that uniquely identifies the original stream.\n   */\n  public final String key;\n  /**\n   * The position of the {@link CacheSpan} in the original stream.\n   */\n  public final long position;\n  /**\n   * The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an open-ended hole.\n   */\n  public final long length;\n  /**\n   * Whether the {@link CacheSpan} is cached.\n   */\n  public final boolean isCached;\n  /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */\n  @Nullable public final File file;\n  /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */\n  public final long lastTouchTimestamp;\n\n  /**\n   * Creates a hole CacheSpan which isn't cached, has no last touch timestamp and no file\n   * associated.\n   *\n   * @param key The cache key that uniquely identifies the original stream.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an\n   *     open-ended hole.\n   */\n  public CacheSpan(String key, long position, long length) {\n    this(key, position, length, C.TIME_UNSET, null);\n  }\n\n  /**\n   * Creates a CacheSpan.\n   *\n   * @param key The cache key that uniquely identifies the original stream.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an\n   *     open-ended hole.\n   * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link\n   *     #isCached} is false.\n   * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.\n   */\n  public CacheSpan(\n      String key, long position, long length, long lastTouchTimestamp, @Nullable File file) {\n    this.key = key;\n    this.position = position;\n    this.length = length;\n    this.isCached = file != null;\n    this.file = file;\n    this.lastTouchTimestamp = lastTouchTimestamp;\n  }\n\n  /**\n   * Returns whether this is an open-ended {@link CacheSpan}.\n   */\n  public boolean isOpenEnded() {\n    return length == C.LENGTH_UNSET;\n  }\n\n  /**\n   * Returns whether this is a hole {@link CacheSpan}.\n   */\n  public boolean isHoleSpan() {\n    return !isCached;\n  }\n\n  @Override\n  public int compareTo(@NonNull CacheSpan another) {\n    if (!key.equals(another.key)) {\n      return key.compareTo(another.key);\n    }\n    long startOffsetDiff = position - another.position;\n    return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.net.Uri;\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.NavigableSet;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * Caching related utility methods.\n */\npublic final class CacheUtil {\n\n  /** Receives progress updates during cache operations. */\n  public interface ProgressListener {\n\n    /**\n     * Called when progress is made during a cache operation.\n     *\n     * @param requestLength The length of the content being cached in bytes, or {@link\n     *     C#LENGTH_UNSET} if unknown.\n     * @param bytesCached The number of bytes that are cached.\n     * @param newBytesCached The number of bytes that have been newly cached since the last progress\n     *     update.\n     */\n    void onProgress(long requestLength, long bytesCached, long newBytesCached);\n  }\n\n  /** Default buffer size to be used while caching. */\n  public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;\n\n  /** Default {@link CacheKeyFactory}. */\n  public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY =\n      (dataSpec) -> dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri);\n\n  /**\n   * Generates a cache key out of the given {@link Uri}.\n   *\n   * @param uri Uri of a content which the requested key is for.\n   */\n  public static String generateKey(Uri uri) {\n    return uri.toString();\n  }\n\n  /**\n   * Queries the cache to obtain the request length and the number of bytes already cached for a\n   * given {@link DataSpec}.\n   *\n   * @param dataSpec Defines the data to be checked.\n   * @param cache A {@link Cache} which has the data.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   * @return A pair containing the request length and the number of bytes that are already cached.\n   */\n  public static Pair<Long, Long> getCached(\n      DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {\n    String key = buildCacheKey(dataSpec, cacheKeyFactory);\n    long position = dataSpec.absoluteStreamPosition;\n    long requestLength = getRequestLength(dataSpec, cache, key);\n    long bytesAlreadyCached = 0;\n    long bytesLeft = requestLength;\n    while (bytesLeft != 0) {\n      long blockLength =\n          cache.getCachedLength(\n              key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);\n      if (blockLength > 0) {\n        bytesAlreadyCached += blockLength;\n      } else {\n        blockLength = -blockLength;\n        if (blockLength == Long.MAX_VALUE) {\n          break;\n        }\n      }\n      position += blockLength;\n      bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;\n    }\n    return Pair.create(requestLength, bytesAlreadyCached);\n  }\n\n  /**\n   * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early\n   * if the end of the input is reached.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param dataSpec Defines the data to be cached.\n   * @param cache A {@link Cache} to store the data.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   * @param upstream A {@link DataSource} for reading data not in the cache.\n   * @param progressListener A listener to receive progress updates, or {@code null}.\n   * @param isCanceled An optional flag that will interrupt caching if set to true.\n   * @throws IOException If an error occurs reading from the source.\n   * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.\n   */\n  @WorkerThread\n  public static void cache(\n      DataSpec dataSpec,\n      Cache cache,\n      @Nullable CacheKeyFactory cacheKeyFactory,\n      DataSource upstream,\n      @Nullable ProgressListener progressListener,\n      @Nullable AtomicBoolean isCanceled)\n      throws IOException, InterruptedException {\n    cache(\n        dataSpec,\n        cache,\n        cacheKeyFactory,\n        new CacheDataSource(cache, upstream),\n        new byte[DEFAULT_BUFFER_SIZE_BYTES],\n        /* priorityTaskManager= */ null,\n        /* priority= */ 0,\n        progressListener,\n        isCanceled,\n        /* enableEOFException= */ false);\n  }\n\n  /**\n   * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops\n   * early if end of input is reached and {@code enableEOFException} is false.\n   *\n   * <p>If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending\n   * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager.\n   * Please note that it's the responsibility of the calling code to call {@link\n   * PriorityTaskManager#add} to register with the manager before calling this method, and to call\n   * {@link PriorityTaskManager#remove} afterwards to unregister.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param dataSpec Defines the data to be cached.\n   * @param cache A {@link Cache} to store the data.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   * @param dataSource A {@link CacheDataSource} that works on the {@code cache}.\n   * @param buffer The buffer to be used while caching.\n   * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with\n   *     caching.\n   * @param priority The priority of this task. Used with {@code priorityTaskManager}.\n   * @param progressListener A listener to receive progress updates, or {@code null}.\n   * @param isCanceled An optional flag that will interrupt caching if set to true.\n   * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been\n   *     reached unexpectedly.\n   * @throws IOException If an error occurs reading from the source.\n   * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.\n   */\n  @WorkerThread\n  public static void cache(\n      DataSpec dataSpec,\n      Cache cache,\n      @Nullable CacheKeyFactory cacheKeyFactory,\n      CacheDataSource dataSource,\n      byte[] buffer,\n      @Nullable PriorityTaskManager priorityTaskManager,\n      int priority,\n      @Nullable ProgressListener progressListener,\n      @Nullable AtomicBoolean isCanceled,\n      boolean enableEOFException)\n      throws IOException, InterruptedException {\n    Assertions.checkNotNull(dataSource);\n    Assertions.checkNotNull(buffer);\n\n    String key = buildCacheKey(dataSpec, cacheKeyFactory);\n    long bytesLeft;\n    ProgressNotifier progressNotifier = null;\n    if (progressListener != null) {\n      progressNotifier = new ProgressNotifier(progressListener);\n      Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);\n      progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);\n      bytesLeft = lengthAndBytesAlreadyCached.first;\n    } else {\n      bytesLeft = getRequestLength(dataSpec, cache, key);\n    }\n\n    long position = dataSpec.absoluteStreamPosition;\n    boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;\n    while (bytesLeft != 0) {\n      throwExceptionIfInterruptedOrCancelled(isCanceled);\n      long blockLength =\n          cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);\n      if (blockLength > 0) {\n        // Skip already cached data.\n      } else {\n        // There is a hole in the cache which is at least \"-blockLength\" long.\n        blockLength = -blockLength;\n        long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;\n        boolean isLastBlock = length == bytesLeft;\n        long read =\n            readAndDiscard(\n                dataSpec,\n                position,\n                length,\n                dataSource,\n                buffer,\n                priorityTaskManager,\n                priority,\n                progressNotifier,\n                isLastBlock,\n                isCanceled);\n        if (read < blockLength) {\n          // Reached to the end of the data.\n          if (enableEOFException && !lengthUnset) {\n            throw new EOFException();\n          }\n          break;\n        }\n      }\n      position += blockLength;\n      if (!lengthUnset) {\n        bytesLeft -= blockLength;\n      }\n    }\n  }\n\n  private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) {\n    if (dataSpec.length != C.LENGTH_UNSET) {\n      return dataSpec.length;\n    } else {\n      long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));\n      return contentLength == C.LENGTH_UNSET\n          ? C.LENGTH_UNSET\n          : contentLength - dataSpec.absoluteStreamPosition;\n    }\n  }\n\n  /**\n   * Reads and discards all data specified by the {@code dataSpec}.\n   *\n   * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length}\n   *     fields are overwritten by the following parameters.\n   * @param absoluteStreamPosition The absolute position of the data to be read.\n   * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.\n   * @param dataSource The {@link DataSource} to read the data from.\n   * @param buffer The buffer to be used while downloading.\n   * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with\n   *     caching.\n   * @param priority The priority of this task.\n   * @param progressNotifier A notifier through which to report progress updates, or {@code null}.\n   * @param isLastBlock Whether this read block is the last block of the content.\n   * @param isCanceled An optional flag that will interrupt caching if set to true.\n   * @return Number of read bytes, or 0 if no data is available because the end of the opened range\n   *     has been reached.\n   */\n  private static long readAndDiscard(\n      DataSpec dataSpec,\n      long absoluteStreamPosition,\n      long length,\n      DataSource dataSource,\n      byte[] buffer,\n      @Nullable PriorityTaskManager priorityTaskManager,\n      int priority,\n      @Nullable ProgressNotifier progressNotifier,\n      boolean isLastBlock,\n      @Nullable AtomicBoolean isCanceled)\n      throws IOException, InterruptedException {\n    long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;\n    long initialPositionOffset = positionOffset;\n    long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;\n    while (true) {\n      if (priorityTaskManager != null) {\n        // Wait for any other thread with higher priority to finish its job.\n        priorityTaskManager.proceed(priority);\n      }\n      throwExceptionIfInterruptedOrCancelled(isCanceled);\n      try {\n        long resolvedLength = C.LENGTH_UNSET;\n        boolean isDataSourceOpen = false;\n        if (endOffset != C.POSITION_UNSET) {\n          // If a specific length is given, first try to open the data source for that length to\n          // avoid more data then required to be requested. If the given length exceeds the end of\n          // input we will get a \"position out of range\" error. In that case try to open the source\n          // again with unset length.\n          try {\n            resolvedLength =\n                dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));\n            isDataSourceOpen = true;\n          } catch (IOException exception) {\n            if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {\n              throw exception;\n            }\n            Util.closeQuietly(dataSource);\n          }\n        }\n        if (!isDataSourceOpen) {\n          resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET));\n        }\n        if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {\n          progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);\n        }\n        while (positionOffset != endOffset) {\n          throwExceptionIfInterruptedOrCancelled(isCanceled);\n          int bytesRead =\n              dataSource.read(\n                  buffer,\n                  0,\n                  endOffset != C.POSITION_UNSET\n                      ? (int) Math.min(buffer.length, endOffset - positionOffset)\n                      : buffer.length);\n          if (bytesRead == C.RESULT_END_OF_INPUT) {\n            if (progressNotifier != null) {\n              progressNotifier.onRequestLengthResolved(positionOffset);\n            }\n            break;\n          }\n          positionOffset += bytesRead;\n          if (progressNotifier != null) {\n            progressNotifier.onBytesCached(bytesRead);\n          }\n        }\n        return positionOffset - initialPositionOffset;\n      } catch (PriorityTaskManager.PriorityTooLowException exception) {\n        // catch and try again\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n    }\n  }\n\n  /**\n   * Removes all of the data specified by the {@code dataSpec}.\n   *\n   * <p>This methods blocks until the operation is complete.\n   *\n   * @param dataSpec Defines the data to be removed.\n   * @param cache A {@link Cache} to store the data.\n   * @param cacheKeyFactory An optional factory for cache keys.\n   */\n  @WorkerThread\n  public static void remove(\n      DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {\n    remove(cache, buildCacheKey(dataSpec, cacheKeyFactory));\n  }\n\n  /**\n   * Removes all of the data specified by the {@code key}.\n   *\n   * <p>This methods blocks until the operation is complete.\n   *\n   * @param cache A {@link Cache} to store the data.\n   * @param key The key whose data should be removed.\n   */\n  @WorkerThread\n  public static void remove(Cache cache, String key) {\n    NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(key);\n    for (CacheSpan cachedSpan : cachedSpans) {\n      try {\n        cache.removeSpan(cachedSpan);\n      } catch (Cache.CacheException e) {\n        // Do nothing.\n      }\n    }\n  }\n\n  /* package */ static boolean isCausedByPositionOutOfRange(IOException e) {\n    Throwable cause = e;\n    while (cause != null) {\n      if (cause instanceof DataSourceException) {\n        int reason = ((DataSourceException) cause).reason;\n        if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {\n          return true;\n        }\n      }\n      cause = cause.getCause();\n    }\n    return false;\n  }\n\n  private static String buildCacheKey(\n      DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {\n    return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)\n        .buildCacheKey(dataSpec);\n  }\n\n  private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled)\n      throws InterruptedException {\n    if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) {\n      throw new InterruptedException();\n    }\n  }\n\n  private CacheUtil() {}\n\n  private static final class ProgressNotifier {\n    /** The listener to notify when progress is made. */\n    private final ProgressListener listener;\n    /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */\n    private long requestLength;\n    /** The number of bytes that are cached. */\n    private long bytesCached;\n\n    public ProgressNotifier(ProgressListener listener) {\n      this.listener = listener;\n    }\n\n    public void init(long requestLength, long bytesCached) {\n      this.requestLength = requestLength;\n      this.bytesCached = bytesCached;\n      listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);\n    }\n\n    public void onRequestLengthResolved(long requestLength) {\n      if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) {\n        this.requestLength = requestLength;\n        listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);\n      }\n    }\n\n    public void onBytesCached(long newBytesCached) {\n      bytesCached += newBytesCached;\n      listener.onProgress(requestLength, bytesCached, newBytesCached);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport java.io.File;\nimport java.util.TreeSet;\n\n/** Defines the cached content for a single stream. */\n/* package */ final class CachedContent {\n\n  private static final String TAG = \"CachedContent\";\n\n  /** The cache file id that uniquely identifies the original stream. */\n  public final int id;\n  /** The cache key that uniquely identifies the original stream. */\n  public final String key;\n  /** The cached spans of this content. */\n  private final TreeSet<SimpleCacheSpan> cachedSpans;\n  /** Metadata values. */\n  private DefaultContentMetadata metadata;\n  /** Whether the content is locked. */\n  private boolean locked;\n\n  /**\n   * Creates a CachedContent.\n   *\n   * @param id The cache file id.\n   * @param key The cache stream key.\n   */\n  public CachedContent(int id, String key) {\n    this(id, key, DefaultContentMetadata.EMPTY);\n  }\n\n  public CachedContent(int id, String key, DefaultContentMetadata metadata) {\n    this.id = id;\n    this.key = key;\n    this.metadata = metadata;\n    this.cachedSpans = new TreeSet<>();\n  }\n\n  /** Returns the metadata. */\n  public DefaultContentMetadata getMetadata() {\n    return metadata;\n  }\n\n  /**\n   * Applies {@code mutations} to the metadata.\n   *\n   * @return Whether {@code mutations} changed any metadata.\n   */\n  public boolean applyMetadataMutations(ContentMetadataMutations mutations) {\n    DefaultContentMetadata oldMetadata = metadata;\n    metadata = metadata.copyWithMutationsApplied(mutations);\n    return !metadata.equals(oldMetadata);\n  }\n\n  /** Returns whether the content is locked. */\n  public boolean isLocked() {\n    return locked;\n  }\n\n  /** Sets the locked state of the content. */\n  public void setLocked(boolean locked) {\n    this.locked = locked;\n  }\n\n  /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */\n  public void addSpan(SimpleCacheSpan span) {\n    cachedSpans.add(span);\n  }\n\n  /** Returns a set of all {@link SimpleCacheSpan}s. */\n  public TreeSet<SimpleCacheSpan> getSpans() {\n    return cachedSpans;\n  }\n\n  /**\n   * Returns the span containing the position. If there isn't one, it returns a hole span\n   * which defines the maximum extents of the hole in the cache.\n   */\n  public SimpleCacheSpan getSpan(long position) {\n    SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);\n    SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);\n    if (floorSpan != null && floorSpan.position + floorSpan.length > position) {\n      return floorSpan;\n    }\n    SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan);\n    return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position)\n        : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position);\n  }\n\n  /**\n   * Returns the length of the cached data block starting from the {@code position} to the block end\n   * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap\n   * to the next cached data up to {@code length} bytes) is returned.\n   *\n   * @param position The starting position of the data.\n   * @param length The maximum length of the data to be returned.\n   * @return the length of the cached or not cached data block length.\n   */\n  public long getCachedBytesLength(long position, long length) {\n    SimpleCacheSpan span = getSpan(position);\n    if (span.isHoleSpan()) {\n      // We don't have a span covering the start of the queried region.\n      return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);\n    }\n    long queryEndPosition = position + length;\n    long currentEndPosition = span.position + span.length;\n    if (currentEndPosition < queryEndPosition) {\n      for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) {\n        if (next.position > currentEndPosition) {\n          // There's a hole in the cache within the queried region.\n          break;\n        }\n        // We expect currentEndPosition to always equal (next.position + next.length), but\n        // perform a max check anyway to guard against the existence of overlapping spans.\n        currentEndPosition = Math.max(currentEndPosition, next.position + next.length);\n        if (currentEndPosition >= queryEndPosition) {\n          // We've found spans covering the queried region.\n          break;\n        }\n      }\n    }\n    return Math.min(currentEndPosition - position, length);\n  }\n\n  /**\n   * Sets the given span's last touch timestamp. The passed span becomes invalid after this call.\n   *\n   * @param cacheSpan Span to be copied and updated.\n   * @param lastTouchTimestamp The new last touch timestamp.\n   * @param updateFile Whether the span file should be renamed to have its timestamp match the new\n   *     last touch time.\n   * @return A span with the updated last touch timestamp.\n   */\n  public SimpleCacheSpan setLastTouchTimestamp(\n      SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {\n    Assertions.checkState(cachedSpans.remove(cacheSpan));\n    File file = cacheSpan.file;\n    if (updateFile) {\n      File directory = file.getParentFile();\n      long position = cacheSpan.position;\n      File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp);\n      if (file.renameTo(newFile)) {\n        file = newFile;\n      } else {\n        Log.w(TAG, \"Failed to rename \" + file + \" to \" + newFile);\n      }\n    }\n    SimpleCacheSpan newCacheSpan =\n        cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp);\n    cachedSpans.add(newCacheSpan);\n    return newCacheSpan;\n  }\n\n  /** Returns whether there are any spans cached. */\n  public boolean isEmpty() {\n    return cachedSpans.isEmpty();\n  }\n\n  /** Removes the given span from cache. */\n  public boolean removeSpan(CacheSpan span) {\n    if (cachedSpans.remove(span)) {\n      span.file.delete();\n      return true;\n    }\n    return false;\n  }\n\n  @Override\n  public int hashCode() {\n    int result = id;\n    result = 31 * result + key.hashCode();\n    result = 31 * result + metadata.hashCode();\n    return result;\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    CachedContent that = (CachedContent) o;\n    return id == that.id\n        && key.equals(that.key)\n        && cachedSpans.equals(that.cachedSpans)\n        && metadata.equals(that.metadata);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.annotation.SuppressLint;\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.database.DatabaseIOException;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.database.VersionTable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.AtomicFile;\nimport com.google.android.exoplayer2.util.ReusableBufferedOutputStream;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.DataInputStream;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.Set;\nimport javax.crypto.Cipher;\nimport javax.crypto.CipherInputStream;\nimport javax.crypto.CipherOutputStream;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** Maintains the index of cached content. */\n/* package */ class CachedContentIndex {\n\n  /* package */ static final String FILE_NAME_ATOMIC = \"cached_content_index.exi\";\n\n  private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;\n\n  private final HashMap<String, CachedContent> keyToContent;\n  /**\n   * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that\n   * have been removed from the index since it was last stored. This prevents reuse of these ids,\n   * which is necessary to avoid clashes that could otherwise occur as a result of the sequence:\n   *\n   * <p>[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ...\n   * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for\n   * key2 is partially written using a path corresponding to id1 ... the process is killed before\n   * the index is stored to disk ... [4] The index is read from disk, causing the partially written\n   * file to be incorrectly associated to key1\n   *\n   * <p>By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete\n   * the partially written file because the index does not contain an entry for id2.\n   *\n   * <p>When the index is next stored (id -> null) entries are removed, making the ids eligible for\n   * reuse.\n   */\n  private final SparseArray<@NullableType String> idToKey;\n  /**\n   * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed\n   * efficiently when the index is next stored.\n   */\n  private final SparseBooleanArray removedIds;\n  /** Tracks ids that are new since the index was last stored. */\n  private final SparseBooleanArray newIds;\n\n  private Storage storage;\n  @Nullable private Storage previousStorage;\n\n  /** Returns whether the file is an index file. */\n  public static boolean isIndexFile(String fileName) {\n    // Atomic file backups add additional suffixes to the file name.\n    return fileName.startsWith(FILE_NAME_ATOMIC);\n  }\n\n  /**\n   * Deletes index data for the specified cache.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param databaseProvider Provides the database in which the index is stored.\n   * @param uid The cache UID.\n   * @throws DatabaseIOException If an error occurs deleting the index data.\n   */\n  @WorkerThread\n  public static void delete(DatabaseProvider databaseProvider, long uid)\n      throws DatabaseIOException {\n    DatabaseStorage.delete(databaseProvider, uid);\n  }\n\n  /**\n   * Creates an instance supporting database storage only.\n   *\n   * @param databaseProvider Provides the database in which the index is stored.\n   */\n  public CachedContentIndex(DatabaseProvider databaseProvider) {\n    this(\n        databaseProvider,\n        /* legacyStorageDir= */ null,\n        /* legacyStorageSecretKey= */ null,\n        /* legacyStorageEncrypt= */ false,\n        /* preferLegacyStorage= */ false);\n  }\n\n  /**\n   * Creates an instance supporting either or both of database and legacy storage.\n   *\n   * @param databaseProvider Provides the database in which the index is stored, or {@code null} to\n   *     use only legacy storage.\n   * @param legacyStorageDir The directory in which any legacy storage is stored, or {@code null} to\n   *     use only database storage.\n   * @param legacyStorageSecretKey A 16 byte AES key for reading, and optionally writing, legacy\n   *     storage.\n   * @param legacyStorageEncrypt Whether to encrypt when writing to legacy storage. Must be false if\n   *     {@code legacyStorageSecretKey} is null.\n   * @param preferLegacyStorage Whether to use prefer legacy storage if both storage types are\n   *     enabled. This option is only useful for downgrading from database storage back to legacy\n   *     storage.\n   */\n  public CachedContentIndex(\n      @Nullable DatabaseProvider databaseProvider,\n      @Nullable File legacyStorageDir,\n      @Nullable byte[] legacyStorageSecretKey,\n      boolean legacyStorageEncrypt,\n      boolean preferLegacyStorage) {\n    Assertions.checkState(databaseProvider != null || legacyStorageDir != null);\n    keyToContent = new HashMap<>();\n    idToKey = new SparseArray<>();\n    removedIds = new SparseBooleanArray();\n    newIds = new SparseBooleanArray();\n    Storage databaseStorage =\n        databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;\n    Storage legacyStorage =\n        legacyStorageDir != null\n            ? new LegacyStorage(\n                new File(legacyStorageDir, FILE_NAME_ATOMIC),\n                legacyStorageSecretKey,\n                legacyStorageEncrypt)\n            : null;\n    if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {\n      storage = legacyStorage;\n      previousStorage = databaseStorage;\n    } else {\n      storage = databaseStorage;\n      previousStorage = legacyStorage;\n    }\n  }\n\n  /**\n   * Loads the index data for the given cache UID.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param uid The UID of the cache whose index is to be loaded.\n   * @throws IOException If an error occurs initializing the index data.\n   */\n  @WorkerThread\n  public void initialize(long uid) throws IOException {\n    storage.initialize(uid);\n    if (previousStorage != null) {\n      previousStorage.initialize(uid);\n    }\n    if (!storage.exists() && previousStorage != null && previousStorage.exists()) {\n      // Copy from previous storage into current storage.\n      previousStorage.load(keyToContent, idToKey);\n      storage.storeFully(keyToContent);\n    } else {\n      // Load from the current storage.\n      storage.load(keyToContent, idToKey);\n    }\n    if (previousStorage != null) {\n      previousStorage.delete();\n      previousStorage = null;\n    }\n  }\n\n  /**\n   * Stores the index data to index file if there is a change.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @throws IOException If an error occurs storing the index data.\n   */\n  @WorkerThread\n  public void store() throws IOException {\n    storage.storeIncremental(keyToContent);\n    // Make ids that were removed since the index was last stored eligible for re-use.\n    int removedIdCount = removedIds.size();\n    for (int i = 0; i < removedIdCount; i++) {\n      idToKey.remove(removedIds.keyAt(i));\n    }\n    removedIds.clear();\n    newIds.clear();\n  }\n\n  /**\n   * Adds the given key to the index if it isn't there already.\n   *\n   * @param key The cache key that uniquely identifies the original stream.\n   * @return A new or existing CachedContent instance with the given key.\n   */\n  public CachedContent getOrAdd(String key) {\n    CachedContent cachedContent = keyToContent.get(key);\n    return cachedContent == null ? addNew(key) : cachedContent;\n  }\n\n  /** Returns a CachedContent instance with the given key or null if there isn't one. */\n  public CachedContent get(String key) {\n    return keyToContent.get(key);\n  }\n\n  /**\n   * Returns a Collection of all CachedContent instances in the index. The collection is backed by\n   * the {@code keyToContent} map, so changes to the map are reflected in the collection, and\n   * vice-versa. If the map is modified while an iteration over the collection is in progress\n   * (except through the iterator's own remove operation), the results of the iteration are\n   * undefined.\n   */\n  public Collection<CachedContent> getAll() {\n    return keyToContent.values();\n  }\n\n  /** Returns an existing or new id assigned to the given key. */\n  public int assignIdForKey(String key) {\n    return getOrAdd(key).id;\n  }\n\n  /** Returns the key which has the given id assigned. */\n  public String getKeyForId(int id) {\n    return idToKey.get(id);\n  }\n\n  /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */\n  public void maybeRemove(String key) {\n    CachedContent cachedContent = keyToContent.get(key);\n    if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {\n      keyToContent.remove(key);\n      int id = cachedContent.id;\n      boolean neverStored = newIds.get(id);\n      storage.onRemove(cachedContent, neverStored);\n      if (neverStored) {\n        // The id can be reused immediately.\n        idToKey.remove(id);\n        newIds.delete(id);\n      } else {\n        // Keep an entry in idToKey to stop the id from being reused until the index is next stored,\n        // and add an entry to removedIds to track that it should be removed when this does happen.\n        idToKey.put(id, /* value= */ null);\n        removedIds.put(id, /* value= */ true);\n      }\n    }\n  }\n\n  /** Removes empty and not locked {@link CachedContent} instances from index. */\n  public void removeEmpty() {\n    String[] keys = new String[keyToContent.size()];\n    keyToContent.keySet().toArray(keys);\n    for (String key : keys) {\n      maybeRemove(key);\n    }\n  }\n\n  /**\n   * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so\n   * changes to the map are reflected in the set, and vice-versa. If the map is modified while an\n   * iteration over the set is in progress (except through the iterator's own remove operation), the\n   * results of the iteration are undefined.\n   */\n  public Set<String> getKeys() {\n    return keyToContent.keySet();\n  }\n\n  /**\n   * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link\n   * CachedContent} is added if there isn't one already with the given key.\n   */\n  public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) {\n    CachedContent cachedContent = getOrAdd(key);\n    if (cachedContent.applyMetadataMutations(mutations)) {\n      storage.onUpdate(cachedContent);\n    }\n  }\n\n  /** Returns a {@link ContentMetadata} for the given key. */\n  public ContentMetadata getContentMetadata(String key) {\n    CachedContent cachedContent = get(key);\n    return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;\n  }\n\n  private CachedContent addNew(String key) {\n    int id = getNewId(idToKey);\n    CachedContent cachedContent = new CachedContent(id, key);\n    keyToContent.put(key, cachedContent);\n    idToKey.put(id, key);\n    newIds.put(id, true);\n    storage.onUpdate(cachedContent);\n    return cachedContent;\n  }\n\n  @SuppressLint(\"GetInstance\") // Suppress warning about specifying \"BC\" as an explicit provider.\n  private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {\n    // Workaround for https://issuetracker.google.com/issues/36976726\n    if (Util.SDK_INT == 18) {\n      try {\n        return Cipher.getInstance(\"AES/CBC/PKCS5PADDING\", \"BC\");\n      } catch (Throwable ignored) {\n        // ignored\n      }\n    }\n    return Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");\n  }\n\n  /**\n   * Returns an id which isn't used in the given array. If the maximum id in the array is smaller\n   * than {@link Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it\n   * returns the smallest unused non-negative integer.\n   */\n  @VisibleForTesting\n  /* package */ static int getNewId(SparseArray<String> idToKey) {\n    int size = idToKey.size();\n    int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);\n    if (id < 0) { // In case if we pass max int value.\n      // TODO optimization: defragmentation or binary search?\n      for (id = 0; id < size; id++) {\n        if (id != idToKey.keyAt(id)) {\n          break;\n        }\n      }\n    }\n    return id;\n  }\n\n  /**\n   * Deserializes a {@link DefaultContentMetadata} from the given input stream.\n   *\n   * @param input Input stream to read from.\n   * @return a {@link DefaultContentMetadata} instance.\n   * @throws IOException If an error occurs during reading from the input.\n   */\n  private static DefaultContentMetadata readContentMetadata(DataInputStream input)\n      throws IOException {\n    int size = input.readInt();\n    HashMap<String, byte[]> metadata = new HashMap<>();\n    for (int i = 0; i < size; i++) {\n      String name = input.readUTF();\n      int valueSize = input.readInt();\n      if (valueSize < 0) {\n        throw new IOException(\"Invalid value size: \" + valueSize);\n      }\n      // Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very\n      // large) valueSize was read. In such cases the implementation below is expected to throw\n      // IOException from one of the readFully calls, due to the end of the input being reached.\n      int bytesRead = 0;\n      int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH);\n      byte[] value = Util.EMPTY_BYTE_ARRAY;\n      while (bytesRead != valueSize) {\n        value = Arrays.copyOf(value, bytesRead + nextBytesToRead);\n        input.readFully(value, bytesRead, nextBytesToRead);\n        bytesRead += nextBytesToRead;\n        nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH);\n      }\n      metadata.put(name, value);\n    }\n    return new DefaultContentMetadata(metadata);\n  }\n\n  /**\n   * Serializes itself to a {@link DataOutputStream}.\n   *\n   * @param output Output stream to store the values.\n   * @throws IOException If an error occurs writing to the output.\n   */\n  private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)\n      throws IOException {\n    Set<Map.Entry<String, byte[]>> entrySet = metadata.entrySet();\n    output.writeInt(entrySet.size());\n    for (Map.Entry<String, byte[]> entry : entrySet) {\n      output.writeUTF(entry.getKey());\n      byte[] value = entry.getValue();\n      output.writeInt(value.length);\n      output.write(value);\n    }\n  }\n\n  /** Interface for the persistent index. */\n  private interface Storage {\n\n    /** Initializes the storage for the given cache UID. */\n    void initialize(long uid);\n\n    /**\n     * Returns whether the persisted index exists.\n     *\n     * @throws IOException If an error occurs determining whether the persisted index exists.\n     */\n    boolean exists() throws IOException;\n\n    /**\n     * Deletes the persisted index.\n     *\n     * @throws IOException If an error occurs deleting the index.\n     */\n    void delete() throws IOException;\n\n    /**\n     * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't\n     * already exist.\n     *\n     * <p>If the persisted index is in a permanently bad state (i.e. all further attempts to load it\n     * are also expected to fail) then it will be deleted and the call will return successfully. For\n     * transient failures, {@link IOException} will be thrown.\n     *\n     * @param content The key to content map to populate with persisted data.\n     * @param idToKey The id to key map to populate with persisted data.\n     * @throws IOException If an error occurs loading the index.\n     */\n    void load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)\n        throws IOException;\n\n    /**\n     * Writes the persisted index, creating it if it doesn't already exist and replacing any\n     * existing content if it does.\n     *\n     * @param content The key to content map to persist.\n     * @throws IOException If an error occurs persisting the index.\n     */\n    void storeFully(HashMap<String, CachedContent> content) throws IOException;\n\n    /**\n     * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last\n     * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such\n     * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}.\n     *\n     * @param content The key to content map to persist.\n     * @throws IOException If an error occurs persisting the index.\n     */\n    void storeIncremental(HashMap<String, CachedContent> content) throws IOException;\n\n    /**\n     * Called when a {@link CachedContent} is added or updated.\n     *\n     * @param cachedContent The updated {@link CachedContent}.\n     */\n    void onUpdate(CachedContent cachedContent);\n\n    /**\n     * Called when a {@link CachedContent} is removed.\n     *\n     * @param cachedContent The removed {@link CachedContent}.\n     * @param neverStored True if the {@link CachedContent} was added more recently than when the\n     *     index was last stored.\n     */\n    void onRemove(CachedContent cachedContent, boolean neverStored);\n  }\n\n  /** {@link Storage} implementation that uses an {@link AtomicFile}. */\n  private static class LegacyStorage implements Storage {\n\n    private static final int VERSION = 2;\n    private static final int VERSION_METADATA_INTRODUCED = 2;\n    private static final int FLAG_ENCRYPTED_INDEX = 1;\n\n    private final boolean encrypt;\n    @Nullable private final Cipher cipher;\n    @Nullable private final SecretKeySpec secretKeySpec;\n    @Nullable private final Random random;\n    private final AtomicFile atomicFile;\n\n    private boolean changed;\n    @Nullable private ReusableBufferedOutputStream bufferedOutputStream;\n\n    public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {\n      Cipher cipher = null;\n      SecretKeySpec secretKeySpec = null;\n      if (secretKey != null) {\n        Assertions.checkArgument(secretKey.length == 16);\n        try {\n          cipher = getCipher();\n          secretKeySpec = new SecretKeySpec(secretKey, \"AES\");\n        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {\n          throw new IllegalStateException(e); // Should never happen.\n        }\n      } else {\n        Assertions.checkArgument(!encrypt);\n      }\n      this.encrypt = encrypt;\n      this.cipher = cipher;\n      this.secretKeySpec = secretKeySpec;\n      random = encrypt ? new Random() : null;\n      atomicFile = new AtomicFile(file);\n    }\n\n    @Override\n    public void initialize(long uid) {\n      // Do nothing. Legacy storage uses a separate file for each cache.\n    }\n\n    @Override\n    public boolean exists() {\n      return atomicFile.exists();\n    }\n\n    @Override\n    public void delete() {\n      atomicFile.delete();\n    }\n\n    @Override\n    public void load(\n        HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {\n      Assertions.checkState(!changed);\n      if (!readFile(content, idToKey)) {\n        content.clear();\n        idToKey.clear();\n        atomicFile.delete();\n      }\n    }\n\n    @Override\n    public void storeFully(HashMap<String, CachedContent> content) throws IOException {\n      writeFile(content);\n      changed = false;\n    }\n\n    @Override\n    public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {\n      if (!changed) {\n        return;\n      }\n      storeFully(content);\n    }\n\n    @Override\n    public void onUpdate(CachedContent cachedContent) {\n      changed = true;\n    }\n\n    @Override\n    public void onRemove(CachedContent cachedContent, boolean neverStored) {\n      changed = true;\n    }\n\n    private boolean readFile(\n        HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {\n      if (!atomicFile.exists()) {\n        return true;\n      }\n\n      DataInputStream input = null;\n      try {\n        InputStream inputStream = new BufferedInputStream(atomicFile.openRead());\n        input = new DataInputStream(inputStream);\n        int version = input.readInt();\n        if (version < 0 || version > VERSION) {\n          return false;\n        }\n\n        int flags = input.readInt();\n        if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {\n          if (cipher == null) {\n            return false;\n          }\n          byte[] initializationVector = new byte[16];\n          input.readFully(initializationVector);\n          IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);\n          try {\n            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);\n          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {\n            throw new IllegalStateException(e);\n          }\n          input = new DataInputStream(new CipherInputStream(inputStream, cipher));\n        } else if (encrypt) {\n          changed = true; // Force index to be rewritten encrypted after read.\n        }\n\n        int count = input.readInt();\n        int hashCode = 0;\n        for (int i = 0; i < count; i++) {\n          CachedContent cachedContent = readCachedContent(version, input);\n          content.put(cachedContent.key, cachedContent);\n          idToKey.put(cachedContent.id, cachedContent.key);\n          hashCode += hashCachedContent(cachedContent, version);\n        }\n        int fileHashCode = input.readInt();\n        boolean isEOF = input.read() == -1;\n        if (fileHashCode != hashCode || !isEOF) {\n          return false;\n        }\n      } catch (IOException e) {\n        return false;\n      } finally {\n        if (input != null) {\n          Util.closeQuietly(input);\n        }\n      }\n      return true;\n    }\n\n    private void writeFile(HashMap<String, CachedContent> content) throws IOException {\n      DataOutputStream output = null;\n      try {\n        OutputStream outputStream = atomicFile.startWrite();\n        if (bufferedOutputStream == null) {\n          bufferedOutputStream = new ReusableBufferedOutputStream(outputStream);\n        } else {\n          bufferedOutputStream.reset(outputStream);\n        }\n        output = new DataOutputStream(bufferedOutputStream);\n        output.writeInt(VERSION);\n\n        int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0;\n        output.writeInt(flags);\n\n        if (encrypt) {\n          byte[] initializationVector = new byte[16];\n          random.nextBytes(initializationVector);\n          output.write(initializationVector);\n          IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);\n          try {\n            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);\n          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {\n            throw new IllegalStateException(e); // Should never happen.\n          }\n          output.flush();\n          output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));\n        }\n\n        output.writeInt(content.size());\n        int hashCode = 0;\n        for (CachedContent cachedContent : content.values()) {\n          writeCachedContent(cachedContent, output);\n          hashCode += hashCachedContent(cachedContent, VERSION);\n        }\n        output.writeInt(hashCode);\n        atomicFile.endWrite(output);\n        // Avoid calling close twice. Duplicate CipherOutputStream.close calls did\n        // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/\n        output = null;\n      } finally {\n        Util.closeQuietly(output);\n      }\n    }\n\n    /**\n     * Calculates a hash code for a {@link CachedContent} which is compatible with a particular\n     * index version.\n     */\n    private int hashCachedContent(CachedContent cachedContent, int version) {\n      int result = cachedContent.id;\n      result = 31 * result + cachedContent.key.hashCode();\n      if (version < VERSION_METADATA_INTRODUCED) {\n        long length = ContentMetadata.getContentLength(cachedContent.getMetadata());\n        result = 31 * result + (int) (length ^ (length >>> 32));\n      } else {\n        result = 31 * result + cachedContent.getMetadata().hashCode();\n      }\n      return result;\n    }\n\n    /**\n     * Reads a {@link CachedContent} from a {@link DataInputStream}.\n     *\n     * @param version Version of the encoded data.\n     * @param input Input stream containing values needed to initialize CachedContent instance.\n     * @throws IOException If an error occurs during reading values.\n     */\n    private CachedContent readCachedContent(int version, DataInputStream input) throws IOException {\n      int id = input.readInt();\n      String key = input.readUTF();\n      DefaultContentMetadata metadata;\n      if (version < VERSION_METADATA_INTRODUCED) {\n        long length = input.readLong();\n        ContentMetadataMutations mutations = new ContentMetadataMutations();\n        ContentMetadataMutations.setContentLength(mutations, length);\n        metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);\n      } else {\n        metadata = readContentMetadata(input);\n      }\n      return new CachedContent(id, key, metadata);\n    }\n\n    /**\n     * Writes a {@link CachedContent} to a {@link DataOutputStream}.\n     *\n     * @param output Output stream to store the values.\n     * @throws IOException If an error occurs during writing values to output.\n     */\n    private void writeCachedContent(CachedContent cachedContent, DataOutputStream output)\n        throws IOException {\n      output.writeInt(cachedContent.id);\n      output.writeUTF(cachedContent.key);\n      writeContentMetadata(cachedContent.getMetadata(), output);\n    }\n  }\n\n  /** {@link Storage} implementation that uses an SQL database. */\n  private static final class DatabaseStorage implements Storage {\n\n    private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + \"CacheIndex\";\n    private static final int TABLE_VERSION = 1;\n\n    private static final String COLUMN_ID = \"id\";\n    private static final String COLUMN_KEY = \"key\";\n    private static final String COLUMN_METADATA = \"metadata\";\n\n    private static final int COLUMN_INDEX_ID = 0;\n    private static final int COLUMN_INDEX_KEY = 1;\n    private static final int COLUMN_INDEX_METADATA = 2;\n\n    private static final String WHERE_ID_EQUALS = COLUMN_ID + \" = ?\";\n\n    private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA};\n    private static final String TABLE_SCHEMA =\n        \"(\"\n            + COLUMN_ID\n            + \" INTEGER PRIMARY KEY NOT NULL,\"\n            + COLUMN_KEY\n            + \" TEXT NOT NULL,\"\n            + COLUMN_METADATA\n            + \" BLOB NOT NULL)\";\n\n    private final DatabaseProvider databaseProvider;\n    private final SparseArray<CachedContent> pendingUpdates;\n\n    private String hexUid;\n    private String tableName;\n\n    public static void delete(DatabaseProvider databaseProvider, long uid)\n        throws DatabaseIOException {\n      delete(databaseProvider, Long.toHexString(uid));\n    }\n\n    public DatabaseStorage(DatabaseProvider databaseProvider) {\n      this.databaseProvider = databaseProvider;\n      pendingUpdates = new SparseArray<>();\n    }\n\n    @Override\n    public void initialize(long uid) {\n      hexUid = Long.toHexString(uid);\n      tableName = getTableName(hexUid);\n    }\n\n    @Override\n    public boolean exists() throws DatabaseIOException {\n      return VersionTable.getVersion(\n              databaseProvider.getReadableDatabase(),\n              VersionTable.FEATURE_CACHE_CONTENT_METADATA,\n              hexUid)\n          != VersionTable.VERSION_UNSET;\n    }\n\n    @Override\n    public void delete() throws DatabaseIOException {\n      delete(databaseProvider, hexUid);\n    }\n\n    @Override\n    public void load(\n        HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)\n        throws IOException {\n      Assertions.checkState(pendingUpdates.size() == 0);\n      try {\n        int version =\n            VersionTable.getVersion(\n                databaseProvider.getReadableDatabase(),\n                VersionTable.FEATURE_CACHE_CONTENT_METADATA,\n                hexUid);\n        if (version != TABLE_VERSION) {\n          SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n          writableDatabase.beginTransactionNonExclusive();\n          try {\n            initializeTable(writableDatabase);\n            writableDatabase.setTransactionSuccessful();\n          } finally {\n            writableDatabase.endTransaction();\n          }\n        }\n\n        try (Cursor cursor = getCursor()) {\n          while (cursor.moveToNext()) {\n            int id = cursor.getInt(COLUMN_INDEX_ID);\n            String key = cursor.getString(COLUMN_INDEX_KEY);\n            byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA);\n\n            ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes);\n            DataInputStream input = new DataInputStream(inputStream);\n            DefaultContentMetadata metadata = readContentMetadata(input);\n\n            CachedContent cachedContent = new CachedContent(id, key, metadata);\n            content.put(cachedContent.key, cachedContent);\n            idToKey.put(cachedContent.id, cachedContent.key);\n          }\n        }\n      } catch (SQLiteException e) {\n        content.clear();\n        idToKey.clear();\n        throw new DatabaseIOException(e);\n      }\n    }\n\n    @Override\n    public void storeFully(HashMap<String, CachedContent> content) throws IOException {\n      try {\n        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n        writableDatabase.beginTransactionNonExclusive();\n        try {\n          initializeTable(writableDatabase);\n          for (CachedContent cachedContent : content.values()) {\n            addOrUpdateRow(writableDatabase, cachedContent);\n          }\n          writableDatabase.setTransactionSuccessful();\n          pendingUpdates.clear();\n        } finally {\n          writableDatabase.endTransaction();\n        }\n      } catch (SQLException e) {\n        throw new DatabaseIOException(e);\n      }\n    }\n\n    @Override\n    public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {\n      if (pendingUpdates.size() == 0) {\n        return;\n      }\n      try {\n        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n        writableDatabase.beginTransactionNonExclusive();\n        try {\n          for (int i = 0; i < pendingUpdates.size(); i++) {\n            CachedContent cachedContent = pendingUpdates.valueAt(i);\n            if (cachedContent == null) {\n              deleteRow(writableDatabase, pendingUpdates.keyAt(i));\n            } else {\n              addOrUpdateRow(writableDatabase, cachedContent);\n            }\n          }\n          writableDatabase.setTransactionSuccessful();\n          pendingUpdates.clear();\n        } finally {\n          writableDatabase.endTransaction();\n        }\n      } catch (SQLException e) {\n        throw new DatabaseIOException(e);\n      }\n    }\n\n    @Override\n    public void onUpdate(CachedContent cachedContent) {\n      pendingUpdates.put(cachedContent.id, cachedContent);\n    }\n\n    @Override\n    public void onRemove(CachedContent cachedContent, boolean neverStored) {\n      if (neverStored) {\n        pendingUpdates.delete(cachedContent.id);\n      } else {\n        pendingUpdates.put(cachedContent.id, null);\n      }\n    }\n\n    private Cursor getCursor() {\n      return databaseProvider\n          .getReadableDatabase()\n          .query(\n              tableName,\n              COLUMNS,\n              /* selection= */ null,\n              /* selectionArgs= */ null,\n              /* groupBy= */ null,\n              /* having= */ null,\n              /* orderBy= */ null);\n    }\n\n    private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException {\n      VersionTable.setVersion(\n          writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);\n      dropTable(writableDatabase, tableName);\n      writableDatabase.execSQL(\"CREATE TABLE \" + tableName + \" \" + TABLE_SCHEMA);\n    }\n\n    private void deleteRow(SQLiteDatabase writableDatabase, int key) {\n      writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});\n    }\n\n    private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)\n        throws IOException {\n      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();\n      writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream));\n      byte[] data = outputStream.toByteArray();\n\n      ContentValues values = new ContentValues();\n      values.put(COLUMN_ID, cachedContent.id);\n      values.put(COLUMN_KEY, cachedContent.key);\n      values.put(COLUMN_METADATA, data);\n      writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);\n    }\n\n    private static void delete(DatabaseProvider databaseProvider, String hexUid)\n        throws DatabaseIOException {\n      try {\n        String tableName = getTableName(hexUid);\n        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n        writableDatabase.beginTransactionNonExclusive();\n        try {\n          VersionTable.removeVersion(\n              writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid);\n          dropTable(writableDatabase, tableName);\n          writableDatabase.setTransactionSuccessful();\n        } finally {\n          writableDatabase.endTransaction();\n        }\n      } catch (SQLException e) {\n        throw new DatabaseIOException(e);\n      }\n    }\n\n    private static void dropTable(SQLiteDatabase writableDatabase, String tableName) {\n      writableDatabase.execSQL(\"DROP TABLE IF EXISTS \" + tableName);\n    }\n\n    private static String getTableName(String hexUid) {\n      return TABLE_PREFIX + hexUid;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.NonNull;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.NavigableSet;\nimport java.util.TreeSet;\n\n/**\n * Utility class for efficiently tracking regions of data that are stored in a {@link Cache}\n * for a given cache key.\n */\npublic final class CachedRegionTracker implements Cache.Listener {\n\n  private static final String TAG = \"CachedRegionTracker\";\n\n  public static final int NOT_CACHED = -1;\n  public static final int CACHED_TO_END = -2;\n\n  private final Cache cache;\n  private final String cacheKey;\n  private final ChunkIndex chunkIndex;\n\n  private final TreeSet<Region> regions;\n  private final Region lookupRegion;\n\n  public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) {\n    this.cache = cache;\n    this.cacheKey = cacheKey;\n    this.chunkIndex = chunkIndex;\n    this.regions = new TreeSet<>();\n    this.lookupRegion = new Region(0, 0);\n\n    synchronized (this) {\n      NavigableSet<CacheSpan> cacheSpans = cache.addListener(cacheKey, this);\n      // Merge the spans into regions. mergeSpan is more efficient when merging from high to low,\n      // which is why a descending iterator is used here.\n      Iterator<CacheSpan> spanIterator = cacheSpans.descendingIterator();\n      while (spanIterator.hasNext()) {\n        CacheSpan span = spanIterator.next();\n        mergeSpan(span);\n      }\n    }\n  }\n\n  public void release() {\n    cache.removeListener(cacheKey, this);\n  }\n\n  /**\n   * When provided with a byte offset, this method locates the cached region within which the\n   * offset falls, and returns the approximate end position in milliseconds of that region. If the\n   * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.\n   * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.\n   *\n   * @param byteOffset The byte offset in the underlying stream.\n   * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or\n   *     {@link #CACHED_TO_END}.\n   */\n  public synchronized int getRegionEndTimeMs(long byteOffset) {\n    lookupRegion.startOffset = byteOffset;\n    Region floorRegion = regions.floor(lookupRegion);\n    if (floorRegion == null || byteOffset > floorRegion.endOffset\n        || floorRegion.endOffsetIndex == -1) {\n      return NOT_CACHED;\n    }\n    int index = floorRegion.endOffsetIndex;\n    if (index == chunkIndex.length - 1\n        && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) {\n      return CACHED_TO_END;\n    }\n    long segmentFractionUs = (chunkIndex.durationsUs[index]\n        * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index];\n    return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000);\n  }\n\n  @Override\n  public synchronized void onSpanAdded(Cache cache, CacheSpan span) {\n    mergeSpan(span);\n  }\n\n  @Override\n  public synchronized void onSpanRemoved(Cache cache, CacheSpan span) {\n    Region removedRegion = new Region(span.position, span.position + span.length);\n\n    // Look up a region this span falls into.\n    Region floorRegion = regions.floor(removedRegion);\n    if (floorRegion == null) {\n      Log.e(TAG, \"Removed a span we were not aware of\");\n      return;\n    }\n\n    // Remove it.\n    regions.remove(floorRegion);\n\n    // Add new floor and ceiling regions, if necessary.\n    if (floorRegion.startOffset < removedRegion.startOffset) {\n      Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset);\n\n      int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset);\n      newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index;\n      regions.add(newFloorRegion);\n    }\n\n    if (floorRegion.endOffset > removedRegion.endOffset) {\n      Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset);\n      newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex;\n      regions.add(newCeilingRegion);\n    }\n  }\n\n  @Override\n  public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {\n    // Do nothing.\n  }\n\n  private void mergeSpan(CacheSpan span) {\n    Region newRegion = new Region(span.position, span.position + span.length);\n    Region floorRegion = regions.floor(newRegion);\n    Region ceilingRegion = regions.ceiling(newRegion);\n    boolean floorConnects = regionsConnect(floorRegion, newRegion);\n    boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion);\n\n    if (ceilingConnects) {\n      if (floorConnects) {\n        // Extend floorRegion to cover both newRegion and ceilingRegion.\n        floorRegion.endOffset = ceilingRegion.endOffset;\n        floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;\n      } else {\n        // Extend newRegion to cover ceilingRegion. Add it.\n        newRegion.endOffset = ceilingRegion.endOffset;\n        newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;\n        regions.add(newRegion);\n      }\n      regions.remove(ceilingRegion);\n    } else if (floorConnects) {\n      // Extend floorRegion to the right to cover newRegion.\n      floorRegion.endOffset = newRegion.endOffset;\n      int index = floorRegion.endOffsetIndex;\n      while (index < chunkIndex.length - 1\n          && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) {\n        index++;\n      }\n      floorRegion.endOffsetIndex = index;\n    } else {\n      // This is a new region.\n      int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset);\n      newRegion.endOffsetIndex = index < 0 ? -index - 2 : index;\n      regions.add(newRegion);\n    }\n  }\n\n  private boolean regionsConnect(Region lower, Region upper) {\n    return lower != null && upper != null && lower.endOffset == upper.startOffset;\n  }\n\n  private static class Region implements Comparable<Region> {\n\n    /**\n     * The first byte of the region (inclusive).\n     */\n    public long startOffset;\n    /**\n     * End offset of the region (exclusive).\n     */\n    public long endOffset;\n    /**\n     * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes\n     * before the start of the first media chunk (i.e. if the end offset is within the stream\n     * header).\n     */\n    public int endOffsetIndex;\n\n    public Region(long position, long endOffset) {\n      this.startOffset = position;\n      this.endOffset = endOffset;\n    }\n\n    @Override\n    public int compareTo(@NonNull Region another) {\n      return Util.compareLong(startOffset, another.startOffset);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\n\n/**\n * Interface for an immutable snapshot of keyed metadata.\n */\npublic interface ContentMetadata {\n\n  /**\n   * Prefix for custom metadata keys. Applications can use keys starting with this prefix without\n   * any risk of their keys colliding with ones defined by the ExoPlayer library.\n   */\n  @SuppressWarnings(\"unused\")\n  String KEY_CUSTOM_PREFIX = \"custom_\";\n  /** Key for redirected uri (type: String). */\n  String KEY_REDIRECTED_URI = \"exo_redir\";\n  /** Key for content length in bytes (type: long). */\n  String KEY_CONTENT_LENGTH = \"exo_len\";\n\n  /**\n   * Returns a metadata value.\n   *\n   * @param key Key of the metadata to be returned.\n   * @param defaultValue Value to return if the metadata doesn't exist.\n   * @return The metadata value.\n   */\n  @Nullable\n  byte[] get(String key, @Nullable byte[] defaultValue);\n\n  /**\n   * Returns a metadata value.\n   *\n   * @param key Key of the metadata to be returned.\n   * @param defaultValue Value to return if the metadata doesn't exist.\n   * @return The metadata value.\n   */\n  @Nullable\n  String get(String key, @Nullable String defaultValue);\n\n  /**\n   * Returns a metadata value.\n   *\n   * @param key Key of the metadata to be returned.\n   * @param defaultValue Value to return if the metadata doesn't exist.\n   * @return The metadata value.\n   */\n  long get(String key, long defaultValue);\n\n  /** Returns whether the metadata is available. */\n  boolean contains(String key);\n\n  /**\n   * Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not\n   * set.\n   */\n  static long getContentLength(ContentMetadata contentMetadata) {\n    return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET);\n  }\n\n  /**\n   * Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if\n   * not set.\n   */\n  @Nullable\n  static Uri getRedirectedUri(ContentMetadata contentMetadata) {\n    String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null);\n    return redirectedUri == null ? null : Uri.parse(redirectedUri);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\n/**\n * Defines multiple mutations on metadata value which are applied atomically. This class isn't\n * thread safe.\n */\npublic class ContentMetadataMutations {\n\n  /**\n   * Adds a mutation to set the {@link ContentMetadata#KEY_CONTENT_LENGTH} value, or to remove any\n   * existing value if {@link C#LENGTH_UNSET} is passed.\n   *\n   * @param mutations The mutations to modify.\n   * @param length The length value, or {@link C#LENGTH_UNSET} to remove any existing entry.\n   * @return The mutations instance, for convenience.\n   */\n  public static ContentMetadataMutations setContentLength(\n      ContentMetadataMutations mutations, long length) {\n    return mutations.set(ContentMetadata.KEY_CONTENT_LENGTH, length);\n  }\n\n  /**\n   * Adds a mutation to set the {@link ContentMetadata#KEY_REDIRECTED_URI} value, or to remove any\n   * existing entry if {@code null} is passed.\n   *\n   * @param mutations The mutations to modify.\n   * @param uri The {@link Uri} value, or {@code null} to remove any existing entry.\n   * @return The mutations instance, for convenience.\n   */\n  public static ContentMetadataMutations setRedirectedUri(\n      ContentMetadataMutations mutations, @Nullable Uri uri) {\n    if (uri == null) {\n      return mutations.remove(ContentMetadata.KEY_REDIRECTED_URI);\n    } else {\n      return mutations.set(ContentMetadata.KEY_REDIRECTED_URI, uri.toString());\n    }\n  }\n\n  private final Map<String, Object> editedValues;\n  private final List<String> removedValues;\n\n  /** Constructs a DefaultMetadataMutations. */\n  public ContentMetadataMutations() {\n    editedValues = new HashMap<>();\n    removedValues = new ArrayList<>();\n  }\n\n  /**\n   * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value}\n   * isn't allowed.\n   *\n   * @param name The name of the metadata value.\n   * @param value The value to be set.\n   * @return This instance, for convenience.\n   */\n  public ContentMetadataMutations set(String name, String value) {\n    return checkAndSet(name, value);\n  }\n\n  /**\n   * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed.\n   *\n   * @param name The name of the metadata value.\n   * @param value The value to be set.\n   * @return This instance, for convenience.\n   */\n  public ContentMetadataMutations set(String name, long value) {\n    return checkAndSet(name, value);\n  }\n\n  /**\n   * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value}\n   * isn't allowed.\n   *\n   * @param name The name of the metadata value.\n   * @param value The value to be set.\n   * @return This instance, for convenience.\n   */\n  public ContentMetadataMutations set(String name, byte[] value) {\n    return checkAndSet(name, Arrays.copyOf(value, value.length));\n  }\n\n  /**\n   * Adds a mutation to remove a metadata value.\n   *\n   * @param name The name of the metadata value.\n   * @return This instance, for convenience.\n   */\n  public ContentMetadataMutations remove(String name) {\n    removedValues.add(name);\n    editedValues.remove(name);\n    return this;\n  }\n\n  /** Returns a list of names of metadata values to be removed. */\n  public List<String> getRemovedValues() {\n    return Collections.unmodifiableList(new ArrayList<>(removedValues));\n  }\n\n  /** Returns a map of metadata name, value pairs to be set. Values are copied.  */\n  public Map<String, Object> getEditedValues() {\n    HashMap<String, Object> hashMap = new HashMap<>(editedValues);\n    for (Entry<String, Object> entry : hashMap.entrySet()) {\n      Object value = entry.getValue();\n      if (value instanceof byte[]) {\n        byte[] bytes = (byte[]) value;\n        entry.setValue(Arrays.copyOf(bytes, bytes.length));\n      }\n    }\n    return Collections.unmodifiableMap(hashMap);\n  }\n\n  private ContentMetadataMutations checkAndSet(String name, Object value) {\n    editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value));\n    removedValues.remove(name);\n    return this;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\n\n/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */\npublic final class DefaultContentMetadata implements ContentMetadata {\n\n  /** An empty DefaultContentMetadata. */\n  public static final DefaultContentMetadata EMPTY =\n      new DefaultContentMetadata(Collections.emptyMap());\n\n  private int hashCode;\n\n  private final Map<String, byte[]> metadata;\n\n  public DefaultContentMetadata() {\n    this(Collections.emptyMap());\n  }\n\n  /** @param metadata The metadata entries in their raw byte array form. */\n  public DefaultContentMetadata(Map<String, byte[]> metadata) {\n    this.metadata = Collections.unmodifiableMap(metadata);\n  }\n\n  /**\n   * Returns a copy {@link DefaultContentMetadata} with {@code mutations} applied. If {@code\n   * mutations} don't change anything, returns this instance.\n   */\n  public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {\n    Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations);\n    if (isMetadataEqual(metadata, mutatedMetadata)) {\n      return this;\n    }\n    return new DefaultContentMetadata(mutatedMetadata);\n  }\n\n  /** Returns the set of metadata entries in their raw byte array form. */\n  public Set<Entry<String, byte[]>> entrySet() {\n    return metadata.entrySet();\n  }\n\n  @Override\n  @Nullable\n  public final byte[] get(String name, @Nullable byte[] defaultValue) {\n    if (metadata.containsKey(name)) {\n      byte[] bytes = metadata.get(name);\n      return Arrays.copyOf(bytes, bytes.length);\n    } else {\n      return defaultValue;\n    }\n  }\n\n  @Override\n  @Nullable\n  public final String get(String name, @Nullable String defaultValue) {\n    if (metadata.containsKey(name)) {\n      byte[] bytes = metadata.get(name);\n      return new String(bytes, Charset.forName(C.UTF8_NAME));\n    } else {\n      return defaultValue;\n    }\n  }\n\n  @Override\n  public final long get(String name, long defaultValue) {\n    if (metadata.containsKey(name)) {\n      byte[] bytes = metadata.get(name);\n      return ByteBuffer.wrap(bytes).getLong();\n    } else {\n      return defaultValue;\n    }\n  }\n\n  @Override\n  public final boolean contains(String name) {\n    return metadata.containsKey(name);\n  }\n\n  @Override\n  public boolean equals(@Nullable Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    return isMetadataEqual(metadata, ((DefaultContentMetadata) o).metadata);\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 0;\n      for (Entry<String, byte[]> entry : metadata.entrySet()) {\n        result += entry.getKey().hashCode() ^ Arrays.hashCode(entry.getValue());\n      }\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  private static boolean isMetadataEqual(Map<String, byte[]> first, Map<String, byte[]> second) {\n    if (first.size() != second.size()) {\n      return false;\n    }\n    for (Entry<String, byte[]> entry : first.entrySet()) {\n      byte[] value = entry.getValue();\n      byte[] otherValue = second.get(entry.getKey());\n      if (!Arrays.equals(value, otherValue)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private static Map<String, byte[]> applyMutations(\n      Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {\n    HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);\n    removeValues(metadata, mutations.getRemovedValues());\n    addValues(metadata, mutations.getEditedValues());\n    return metadata;\n  }\n\n  private static void removeValues(HashMap<String, byte[]> metadata, List<String> names) {\n    for (int i = 0; i < names.size(); i++) {\n      metadata.remove(names.get(i));\n    }\n  }\n\n  private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> values) {\n    for (String name : values.keySet()) {\n      metadata.put(name, getBytes(values.get(name)));\n    }\n  }\n\n  private static byte[] getBytes(Object value) {\n    if (value instanceof Long) {\n      return ByteBuffer.allocate(8).putLong((Long) value).array();\n    } else if (value instanceof String) {\n      return ((String) value).getBytes(Charset.forName(C.UTF8_NAME));\n    } else if (value instanceof byte[]) {\n      return (byte[]) value;\n    } else {\n      throw new IllegalArgumentException();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.cache.Cache.CacheException;\nimport java.util.TreeSet;\n\n/** Evicts least recently used cache files first. */\npublic final class LeastRecentlyUsedCacheEvictor implements CacheEvictor {\n\n  private final long maxBytes;\n  private final TreeSet<CacheSpan> leastRecentlyUsed;\n\n  private long currentSize;\n\n  public LeastRecentlyUsedCacheEvictor(long maxBytes) {\n    this.maxBytes = maxBytes;\n    this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare);\n  }\n\n  @Override\n  public boolean requiresCacheSpanTouches() {\n    return true;\n  }\n\n  @Override\n  public void onCacheInitialized() {\n    // Do nothing.\n  }\n\n  @Override\n  public void onStartFile(Cache cache, String key, long position, long length) {\n    if (length != C.LENGTH_UNSET) {\n      evictCache(cache, length);\n    }\n  }\n\n  @Override\n  public void onSpanAdded(Cache cache, CacheSpan span) {\n    leastRecentlyUsed.add(span);\n    currentSize += span.length;\n    evictCache(cache, 0);\n  }\n\n  @Override\n  public void onSpanRemoved(Cache cache, CacheSpan span) {\n    leastRecentlyUsed.remove(span);\n    currentSize -= span.length;\n  }\n\n  @Override\n  public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {\n    onSpanRemoved(cache, oldSpan);\n    onSpanAdded(cache, newSpan);\n  }\n\n  private void evictCache(Cache cache, long requiredSpace) {\n    while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {\n      try {\n        cache.removeSpan(leastRecentlyUsed.first());\n      } catch (CacheException e) {\n        // do nothing.\n      }\n    }\n  }\n\n  private static int compare(CacheSpan lhs, CacheSpan rhs) {\n    long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp;\n    if (lastTouchTimestampDelta == 0) {\n      // Use the standard compareTo method as a tie-break.\n      return lhs.compareTo(rhs);\n    }\n    return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\n\n/**\n * Evictor that doesn't ever evict cache files.\n *\n * Warning: Using this evictor might have unforeseeable consequences if cache\n * size is not managed elsewhere.\n */\npublic final class NoOpCacheEvictor implements CacheEvictor {\n\n  @Override\n  public boolean requiresCacheSpanTouches() {\n    return false;\n  }\n\n  @Override\n  public void onCacheInitialized() {\n    // Do nothing.\n  }\n\n  @Override\n  public void onStartFile(Cache cache, String key, long position, long maxLength) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onSpanAdded(Cache cache, CacheSpan span) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onSpanRemoved(Cache cache, CacheSpan span) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {\n    // Do nothing.\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport android.os.ConditionVariable;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.WorkerThread;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.database.DatabaseIOException;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\nimport java.security.SecureRandom;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.NavigableSet;\nimport java.util.Random;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A {@link Cache} implementation that maintains an in-memory representation.\n *\n * <p>Only one instance of SimpleCache is allowed for a given directory at a given time.\n *\n * <p>To delete a SimpleCache, use {@link #delete(File, DatabaseProvider)} rather than deleting the\n * directory and its contents directly. This is necessary to ensure that associated index data is\n * also removed.\n */\npublic final class SimpleCache implements Cache {\n\n  private static final String TAG = \"SimpleCache\";\n  /**\n   * Cache files are distributed between a number of subdirectories. This helps to avoid poor\n   * performance in cases where the performance of the underlying file system (e.g. FAT32) scales\n   * badly with the number of files per directory. See\n   * https://github.com/google/ExoPlayer/issues/4253.\n   */\n  private static final int SUBDIRECTORY_COUNT = 10;\n\n  private static final String UID_FILE_SUFFIX = \".uid\";\n\n  private static final HashSet<File> lockedCacheDirs = new HashSet<>();\n\n  private final File cacheDir;\n  private final CacheEvictor evictor;\n  private final CachedContentIndex contentIndex;\n  @Nullable private final CacheFileMetadataIndex fileIndex;\n  private final HashMap<String, ArrayList<Listener>> listeners;\n  private final Random random;\n  private final boolean touchCacheSpans;\n\n  private long uid;\n  private long totalSpace;\n  private boolean released;\n  private @MonotonicNonNull CacheException initializationException;\n\n  /**\n   * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the\n   * folder the {@link SimpleCache} instance should be released.\n   */\n  public static synchronized boolean isCacheFolderLocked(File cacheFolder) {\n    return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile());\n  }\n\n  /**\n   * Deletes all content belonging to a cache instance.\n   *\n   * <p>This method may be slow and shouldn't normally be called on the main thread.\n   *\n   * @param cacheDir The cache directory.\n   * @param databaseProvider The database in which index data is stored, or {@code null} if the\n   *     cache used a legacy index.\n   */\n  @WorkerThread\n  public static void delete(File cacheDir, @Nullable DatabaseProvider databaseProvider) {\n    if (!cacheDir.exists()) {\n      return;\n    }\n\n    File[] files = cacheDir.listFiles();\n    if (files == null) {\n      cacheDir.delete();\n      return;\n    }\n\n    if (databaseProvider != null) {\n      // Make a best effort to read the cache UID and delete associated index data before deleting\n      // cache directory itself.\n      long uid = loadUid(files);\n      if (uid != UID_UNSET) {\n        try {\n          CacheFileMetadataIndex.delete(databaseProvider, uid);\n        } catch (DatabaseIOException e) {\n          Log.w(TAG, \"Failed to delete file metadata: \" + uid);\n        }\n        try {\n          CachedContentIndex.delete(databaseProvider, uid);\n        } catch (DatabaseIOException e) {\n          Log.w(TAG, \"Failed to delete file metadata: \" + uid);\n        }\n      }\n    }\n\n    Util.recursiveDelete(cacheDir);\n  }\n\n  /**\n   * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence\n   * the directory cannot be used to store other files.\n   *\n   * @param cacheDir A dedicated cache directory.\n   * @param evictor The evictor to be used. For download use cases where cache eviction should not\n   *     occur, use {@link NoOpCacheEvictor}.\n   * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.\n   */\n  @Deprecated\n  public SimpleCache(File cacheDir, CacheEvictor evictor) {\n    this(cacheDir, evictor, null, false);\n  }\n\n  /**\n   * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence\n   * the directory cannot be used to store other files.\n   *\n   * @param cacheDir A dedicated cache directory.\n   * @param evictor The evictor to be used. For download use cases where cache eviction should not\n   *     occur, use {@link NoOpCacheEvictor}.\n   * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.\n   *     The key must be 16 bytes long.\n   * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public SimpleCache(File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey) {\n    this(cacheDir, evictor, secretKey, secretKey != null);\n  }\n\n  /**\n   * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence\n   * the directory cannot be used to store other files.\n   *\n   * @param cacheDir A dedicated cache directory.\n   * @param evictor The evictor to be used. For download use cases where cache eviction should not\n   *     occur, use {@link NoOpCacheEvictor}.\n   * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.\n   *     The key must be 16 bytes long.\n   * @param encrypt Whether the index will be encrypted when written. Must be false if {@code\n   *     secretKey} is null.\n   * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance.\n   */\n  @Deprecated\n  public SimpleCache(\n      File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey, boolean encrypt) {\n    this(\n        cacheDir,\n        evictor,\n        /* databaseProvider= */ null,\n        secretKey,\n        encrypt,\n        /* preferLegacyIndex= */ true);\n  }\n\n  /**\n   * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence\n   * the directory cannot be used to store other files.\n   *\n   * @param cacheDir A dedicated cache directory.\n   * @param evictor The evictor to be used. For download use cases where cache eviction should not\n   *     occur, use {@link NoOpCacheEvictor}.\n   * @param databaseProvider Provides the database in which the cache index is stored.\n   */\n  public SimpleCache(File cacheDir, CacheEvictor evictor, DatabaseProvider databaseProvider) {\n    this(\n        cacheDir,\n        evictor,\n        databaseProvider,\n        /* legacyIndexSecretKey= */ null,\n        /* legacyIndexEncrypt= */ false,\n        /* preferLegacyIndex= */ false);\n  }\n\n  /**\n   * Constructs the cache. The cache will delete any unrecognized files from the cache directory.\n   * Hence the directory cannot be used to store other files.\n   *\n   * @param cacheDir A dedicated cache directory.\n   * @param evictor The evictor to be used. For download use cases where cache eviction should not\n   *     occur, use {@link NoOpCacheEvictor}.\n   * @param databaseProvider Provides the database in which the cache index is stored, or {@code\n   *     null} to use a legacy index. Using a database index is highly recommended for performance\n   *     reasons.\n   * @param legacyIndexSecretKey A 16 byte AES key for reading, and optionally writing, the legacy\n   *     index. Not used by the database index, however should still be provided when using the\n   *     database index in cases where upgrading from the legacy index may be necessary.\n   * @param legacyIndexEncrypt Whether to encrypt when writing to the legacy index. Must be {@code\n   *     false} if {@code legacyIndexSecretKey} is {@code null}. Not used by the database index.\n   * @param preferLegacyIndex Whether to use the legacy index even if a {@code databaseProvider} is\n   *     provided. Should be {@code false} in nearly all cases. Setting this to {@code true} is only\n   *     useful for downgrading from the database index back to the legacy index.\n   */\n  public SimpleCache(\n      File cacheDir,\n      CacheEvictor evictor,\n      @Nullable DatabaseProvider databaseProvider,\n      @Nullable byte[] legacyIndexSecretKey,\n      boolean legacyIndexEncrypt,\n      boolean preferLegacyIndex) {\n    this(\n        cacheDir,\n        evictor,\n        new CachedContentIndex(\n            databaseProvider,\n            cacheDir,\n            legacyIndexSecretKey,\n            legacyIndexEncrypt,\n            preferLegacyIndex),\n        databaseProvider != null && !preferLegacyIndex\n            ? new CacheFileMetadataIndex(databaseProvider)\n            : null);\n  }\n\n  /* package */ SimpleCache(\n      File cacheDir,\n      CacheEvictor evictor,\n      CachedContentIndex contentIndex,\n      @Nullable CacheFileMetadataIndex fileIndex) {\n    if (!lockFolder(cacheDir)) {\n      throw new IllegalStateException(\"Another SimpleCache instance uses the folder: \" + cacheDir);\n    }\n\n    this.cacheDir = cacheDir;\n    this.evictor = evictor;\n    this.contentIndex = contentIndex;\n    this.fileIndex = fileIndex;\n    listeners = new HashMap<>();\n    random = new Random();\n    touchCacheSpans = evictor.requiresCacheSpanTouches();\n    uid = UID_UNSET;\n\n    // Start cache initialization.\n    final ConditionVariable conditionVariable = new ConditionVariable();\n    new Thread(\"SimpleCache.initialize()\") {\n      @Override\n      public void run() {\n        synchronized (SimpleCache.this) {\n          conditionVariable.open();\n          initialize();\n          SimpleCache.this.evictor.onCacheInitialized();\n        }\n      }\n    }.start();\n    conditionVariable.block();\n  }\n\n  /**\n   * Checks whether the cache was initialized successfully.\n   *\n   * @throws CacheException If an error occurred during initialization.\n   */\n  public synchronized void checkInitialization() throws CacheException {\n    if (initializationException != null) {\n      throw initializationException;\n    }\n  }\n\n  @Override\n  public synchronized long getUid() {\n    return uid;\n  }\n\n  @Override\n  public synchronized void release() {\n    if (released) {\n      return;\n    }\n    listeners.clear();\n    removeStaleSpans();\n    try {\n      contentIndex.store();\n    } catch (IOException e) {\n      Log.e(TAG, \"Storing index file failed\", e);\n    } finally {\n      unlockFolder(cacheDir);\n      released = true;\n    }\n  }\n\n  @Override\n  public synchronized NavigableSet<CacheSpan> addListener(String key, Listener listener) {\n    Assertions.checkState(!released);\n    ArrayList<Listener> listenersForKey = listeners.get(key);\n    if (listenersForKey == null) {\n      listenersForKey = new ArrayList<>();\n      listeners.put(key, listenersForKey);\n    }\n    listenersForKey.add(listener);\n    return getCachedSpans(key);\n  }\n\n  @Override\n  public synchronized void removeListener(String key, Listener listener) {\n    if (released) {\n      return;\n    }\n    ArrayList<Listener> listenersForKey = listeners.get(key);\n    if (listenersForKey != null) {\n      listenersForKey.remove(listener);\n      if (listenersForKey.isEmpty()) {\n        listeners.remove(key);\n      }\n    }\n  }\n\n  @NonNull\n  @Override\n  public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {\n    Assertions.checkState(!released);\n    CachedContent cachedContent = contentIndex.get(key);\n    return cachedContent == null || cachedContent.isEmpty()\n        ? new TreeSet<>()\n        : new TreeSet<CacheSpan>(cachedContent.getSpans());\n  }\n\n  @Override\n  public synchronized Set<String> getKeys() {\n    Assertions.checkState(!released);\n    return new HashSet<>(contentIndex.getKeys());\n  }\n\n  @Override\n  public synchronized long getCacheSpace() {\n    Assertions.checkState(!released);\n    return totalSpace;\n  }\n\n  @Override\n  public synchronized CacheSpan startReadWrite(String key, long position)\n      throws InterruptedException, CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    while (true) {\n      CacheSpan span = startReadWriteNonBlocking(key, position);\n      if (span != null) {\n        return span;\n      } else {\n        // Lock not available. We'll be woken up when a span is added, or when a locked span is\n        // released. We'll be able to make progress when either:\n        // 1. A span is added for the requested key that covers the requested position, in which\n        //    case a read can be started.\n        // 2. The lock for the requested key is released, in which case a write can be started.\n        wait();\n      }\n    }\n  }\n\n  @Override\n  @Nullable\n  public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)\n      throws CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    SimpleCacheSpan span = getSpan(key, position);\n\n    if (span.isCached) {\n      // Read case.\n      return touchSpan(key, span);\n    }\n\n    CachedContent cachedContent = contentIndex.getOrAdd(key);\n    if (!cachedContent.isLocked()) {\n      // Write case.\n      cachedContent.setLocked(true);\n      return span;\n    }\n\n    // Lock not available.\n    return null;\n  }\n\n  @Override\n  public synchronized File startFile(String key, long position, long length) throws CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    CachedContent cachedContent = contentIndex.get(key);\n    Assertions.checkNotNull(cachedContent);\n    Assertions.checkState(cachedContent.isLocked());\n    if (!cacheDir.exists()) {\n      // For some reason the cache directory doesn't exist. Make a best effort to create it.\n      cacheDir.mkdirs();\n      removeStaleSpans();\n    }\n    evictor.onStartFile(this, key, position, length);\n    // Randomly distribute files into subdirectories with a uniform distribution.\n    File fileDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT)));\n    if (!fileDir.exists()) {\n      fileDir.mkdir();\n    }\n    long lastTouchTimestamp = System.currentTimeMillis();\n    return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastTouchTimestamp);\n  }\n\n  @Override\n  public synchronized void commitFile(File file, long length) throws CacheException {\n    Assertions.checkState(!released);\n    if (!file.exists()) {\n      return;\n    }\n    if (length == 0) {\n      file.delete();\n      return;\n    }\n\n    SimpleCacheSpan span =\n        Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));\n    CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));\n    Assertions.checkState(cachedContent.isLocked());\n\n    // Check if the span conflicts with the set content length\n    long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata());\n    if (contentLength != C.LENGTH_UNSET) {\n      Assertions.checkState((span.position + span.length) <= contentLength);\n    }\n\n    if (fileIndex != null) {\n      String fileName = file.getName();\n      try {\n        fileIndex.set(fileName, span.length, span.lastTouchTimestamp);\n      } catch (IOException e) {\n        throw new CacheException(e);\n      }\n    }\n    addSpan(span);\n    try {\n      contentIndex.store();\n    } catch (IOException e) {\n      throw new CacheException(e);\n    }\n    notifyAll();\n  }\n\n  @Override\n  public synchronized void releaseHoleSpan(CacheSpan holeSpan) {\n    Assertions.checkState(!released);\n    CachedContent cachedContent = contentIndex.get(holeSpan.key);\n    Assertions.checkNotNull(cachedContent);\n    Assertions.checkState(cachedContent.isLocked());\n    cachedContent.setLocked(false);\n    contentIndex.maybeRemove(cachedContent.key);\n    notifyAll();\n  }\n\n  @Override\n  public synchronized void removeSpan(CacheSpan span) {\n    Assertions.checkState(!released);\n    removeSpanInternal(span);\n  }\n\n  @Override\n  public synchronized boolean isCached(String key, long position, long length) {\n    Assertions.checkState(!released);\n    CachedContent cachedContent = contentIndex.get(key);\n    return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length;\n  }\n\n  @Override\n  public synchronized long getCachedLength(String key, long position, long length) {\n    Assertions.checkState(!released);\n    CachedContent cachedContent = contentIndex.get(key);\n    return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length;\n  }\n\n  @Override\n  public synchronized void applyContentMetadataMutations(\n      String key, ContentMetadataMutations mutations) throws CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    contentIndex.applyContentMetadataMutations(key, mutations);\n    try {\n      contentIndex.store();\n    } catch (IOException e) {\n      throw new CacheException(e);\n    }\n  }\n\n  @Override\n  public synchronized ContentMetadata getContentMetadata(String key) {\n    Assertions.checkState(!released);\n    return contentIndex.getContentMetadata(key);\n  }\n\n  /** Ensures that the cache's in-memory representation has been initialized. */\n  private void initialize() {\n    if (!cacheDir.exists()) {\n      if (!cacheDir.mkdirs()) {\n        String message = \"Failed to create cache directory: \" + cacheDir;\n        Log.e(TAG, message);\n        initializationException = new CacheException(message);\n        return;\n      }\n    }\n\n    File[] files = cacheDir.listFiles();\n    if (files == null) {\n      String message = \"Failed to list cache directory files: \" + cacheDir;\n      Log.e(TAG, message);\n      initializationException = new CacheException(message);\n      return;\n    }\n\n    uid = loadUid(files);\n    if (uid == UID_UNSET) {\n      try {\n        uid = createUid(cacheDir);\n      } catch (IOException e) {\n        String message = \"Failed to create cache UID: \" + cacheDir;\n        Log.e(TAG, message, e);\n        initializationException = new CacheException(message, e);\n        return;\n      }\n    }\n\n    try {\n      contentIndex.initialize(uid);\n      if (fileIndex != null) {\n        fileIndex.initialize(uid);\n        Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();\n        loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);\n        fileIndex.removeAll(fileMetadata.keySet());\n      } else {\n        loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);\n      }\n    } catch (IOException e) {\n      String message = \"Failed to initialize cache indices: \" + cacheDir;\n      Log.e(TAG, message, e);\n      initializationException = new CacheException(message, e);\n      return;\n    }\n\n    contentIndex.removeEmpty();\n    try {\n      contentIndex.store();\n    } catch (IOException e) {\n      Log.e(TAG, \"Storing index file failed\", e);\n    }\n  }\n\n  /**\n   * Loads a cache directory. If the root directory is passed, also loads any subdirectories.\n   *\n   * @param directory The directory.\n   * @param isRoot Whether the directory is the root directory.\n   * @param files The files belonging to the directory.\n   * @param fileMetadata A mutable map containing cache file metadata, keyed by file name. The map\n   *     is modified by removing entries for all loaded files. When the method call returns, the map\n   *     will contain only metadata that was unused. May be null if no file metadata is available.\n   */\n  private void loadDirectory(\n      File directory,\n      boolean isRoot,\n      @Nullable File[] files,\n      @Nullable Map<String, CacheFileMetadata> fileMetadata) {\n    if (files == null || files.length == 0) {\n      // Either (a) directory isn't really a directory (b) it's empty, or (c) listing files failed.\n      if (!isRoot) {\n        // For (a) and (b) deletion is the desired result. For (c) it will be a no-op if the\n        // directory is non-empty, so there's no harm in trying.\n        directory.delete();\n      }\n      return;\n    }\n    for (File file : files) {\n      String fileName = file.getName();\n      if (isRoot && fileName.indexOf('.') == -1) {\n        loadDirectory(file, /* isRoot= */ false, file.listFiles(), fileMetadata);\n      } else {\n        if (isRoot\n            && (CachedContentIndex.isIndexFile(fileName) || fileName.endsWith(UID_FILE_SUFFIX))) {\n          // Skip expected UID and index files in the root directory.\n          continue;\n        }\n        long length = C.LENGTH_UNSET;\n        long lastTouchTimestamp = C.TIME_UNSET;\n        CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null;\n        if (metadata != null) {\n          length = metadata.length;\n          lastTouchTimestamp = metadata.lastTouchTimestamp;\n        }\n        SimpleCacheSpan span =\n            SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex);\n        if (span != null) {\n          addSpan(span);\n        } else {\n          file.delete();\n        }\n      }\n    }\n  }\n\n  /**\n   * Touches a cache span, returning the updated result. If the evictor does not require cache spans\n   * to be touched, then this method does nothing and the span is returned without modification.\n   *\n   * @param key The key of the span being touched.\n   * @param span The span being touched.\n   * @return The updated span.\n   */\n  private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) {\n    if (!touchCacheSpans) {\n      return span;\n    }\n    String fileName = Assertions.checkNotNull(span.file).getName();\n    long length = span.length;\n    long lastTouchTimestamp = System.currentTimeMillis();\n    boolean updateFile = false;\n    if (fileIndex != null) {\n      try {\n        fileIndex.set(fileName, length, lastTouchTimestamp);\n      } catch (IOException e) {\n        Log.w(TAG, \"Failed to update index with new touch timestamp.\");\n      }\n    } else {\n      // Updating the file itself to incorporate the new last touch timestamp is much slower than\n      // updating the file index. Hence we only update the file if we don't have a file index.\n      updateFile = true;\n    }\n    SimpleCacheSpan newSpan =\n        contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile);\n    notifySpanTouched(span, newSpan);\n    return newSpan;\n  }\n\n  /**\n   * Returns the cache span corresponding to the provided lookup span.\n   *\n   * <p>If the lookup position is contained by an existing entry in the cache, then the returned\n   * span defines the file in which the data is stored. If the lookup position is not contained by\n   * an existing entry, then the returned span defines the maximum extents of the hole in the cache.\n   *\n   * @param key The key of the span being requested.\n   * @param position The position of the span being requested.\n   * @return The corresponding cache {@link SimpleCacheSpan}.\n   */\n  private SimpleCacheSpan getSpan(String key, long position) {\n    CachedContent cachedContent = contentIndex.get(key);\n    if (cachedContent == null) {\n      return SimpleCacheSpan.createOpenHole(key, position);\n    }\n    while (true) {\n      SimpleCacheSpan span = cachedContent.getSpan(position);\n      if (span.isCached && span.file.length() != span.length) {\n        // The file has been modified or deleted underneath us. It's likely that other files will\n        // have been modified too, so scan the whole in-memory representation.\n        removeStaleSpans();\n        continue;\n      }\n      return span;\n    }\n  }\n\n  /**\n   * Adds a cached span to the in-memory representation.\n   *\n   * @param span The span to be added.\n   */\n  private void addSpan(SimpleCacheSpan span) {\n    contentIndex.getOrAdd(span.key).addSpan(span);\n    totalSpace += span.length;\n    notifySpanAdded(span);\n  }\n\n  private void removeSpanInternal(CacheSpan span) {\n    CachedContent cachedContent = contentIndex.get(span.key);\n    if (cachedContent == null || !cachedContent.removeSpan(span)) {\n      return;\n    }\n    totalSpace -= span.length;\n    if (fileIndex != null) {\n      String fileName = span.file.getName();\n      try {\n        fileIndex.remove(fileName);\n      } catch (IOException e) {\n        // This will leave a stale entry in the file index. It will be removed next time the cache\n        // is initialized.\n        Log.w(TAG, \"Failed to remove file index entry for: \" + fileName);\n      }\n    }\n    contentIndex.maybeRemove(cachedContent.key);\n    notifySpanRemoved(span);\n  }\n\n  /**\n   * Scans all of the cached spans in the in-memory representation, removing any for which the\n   * underlying file lengths no longer match.\n   */\n  private void removeStaleSpans() {\n    ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();\n    for (CachedContent cachedContent : contentIndex.getAll()) {\n      for (CacheSpan span : cachedContent.getSpans()) {\n        if (span.file.length() != span.length) {\n          spansToBeRemoved.add(span);\n        }\n      }\n    }\n    for (int i = 0; i < spansToBeRemoved.size(); i++) {\n      removeSpanInternal(spansToBeRemoved.get(i));\n    }\n  }\n\n  private void notifySpanRemoved(CacheSpan span) {\n    ArrayList<Listener> keyListeners = listeners.get(span.key);\n    if (keyListeners != null) {\n      for (int i = keyListeners.size() - 1; i >= 0; i--) {\n        keyListeners.get(i).onSpanRemoved(this, span);\n      }\n    }\n    evictor.onSpanRemoved(this, span);\n  }\n\n  private void notifySpanAdded(SimpleCacheSpan span) {\n    ArrayList<Listener> keyListeners = listeners.get(span.key);\n    if (keyListeners != null) {\n      for (int i = keyListeners.size() - 1; i >= 0; i--) {\n        keyListeners.get(i).onSpanAdded(this, span);\n      }\n    }\n    evictor.onSpanAdded(this, span);\n  }\n\n  private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) {\n    ArrayList<Listener> keyListeners = listeners.get(oldSpan.key);\n    if (keyListeners != null) {\n      for (int i = keyListeners.size() - 1; i >= 0; i--) {\n        keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan);\n      }\n    }\n    evictor.onSpanTouched(this, oldSpan, newSpan);\n  }\n\n  /**\n   * Loads the cache UID from the files belonging to the root directory.\n   *\n   * @param files The files belonging to the root directory.\n   * @return The loaded UID, or {@link #UID_UNSET} if a UID has not yet been created.\n   */\n  private static long loadUid(File[] files) {\n    for (File file : files) {\n      String fileName = file.getName();\n      if (fileName.endsWith(UID_FILE_SUFFIX)) {\n        try {\n          return parseUid(fileName);\n        } catch (NumberFormatException e) {\n          // This should never happen, but if it does delete the malformed UID file and continue.\n          Log.e(TAG, \"Malformed UID file: \" + file);\n          file.delete();\n        }\n      }\n    }\n    return UID_UNSET;\n  }\n\n  @SuppressWarnings(\"TrulyRandom\")\n  private static long createUid(File directory) throws IOException {\n    // Generate a non-negative UID.\n    long uid = new SecureRandom().nextLong();\n    uid = uid == Long.MIN_VALUE ? 0 : Math.abs(uid);\n    // Persist it as a file.\n    String hexUid = Long.toString(uid, /* radix= */ 16);\n    File hexUidFile = new File(directory, hexUid + UID_FILE_SUFFIX);\n    if (!hexUidFile.createNewFile()) {\n      // False means that the file already exists, so this should never happen.\n      throw new IOException(\"Failed to create UID file: \" + hexUidFile);\n    }\n    return uid;\n  }\n\n  private static long parseUid(String fileName) {\n    return Long.parseLong(fileName.substring(0, fileName.indexOf('.')), /* radix= */ 16);\n  }\n\n  private static synchronized boolean lockFolder(File cacheDir) {\n    return lockedCacheDirs.add(cacheDir.getAbsoluteFile());\n  }\n\n  private static synchronized void unlockFolder(File cacheDir) {\n    lockedCacheDirs.remove(cacheDir.getAbsoluteFile());\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.cache;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/** This class stores span metadata in filename. */\n/* package */ final class SimpleCacheSpan extends CacheSpan {\n\n  /* package */ static final String COMMON_SUFFIX = \".exo\";\n\n  private static final String SUFFIX = \".v3\" + COMMON_SUFFIX;\n  private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile(\n      \"^(.+)\\\\.(\\\\d+)\\\\.(\\\\d+)\\\\.v1\\\\.exo$\", Pattern.DOTALL);\n  private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile(\n      \"^(.+)\\\\.(\\\\d+)\\\\.(\\\\d+)\\\\.v2\\\\.exo$\", Pattern.DOTALL);\n  private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile(\n      \"^(\\\\d+)\\\\.(\\\\d+)\\\\.(\\\\d+)\\\\.v3\\\\.exo$\", Pattern.DOTALL);\n\n  /**\n   * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code\n   * timestamp}.\n   *\n   * @param cacheDir The parent abstract pathname.\n   * @param id The cache file id.\n   * @param position The position of the stored data in the original stream.\n   * @param timestamp The file timestamp.\n   * @return The cache file.\n   */\n  public static File getCacheFile(File cacheDir, int id, long position, long timestamp) {\n    return new File(cacheDir, id + \".\" + position + \".\" + timestamp + SUFFIX);\n  }\n\n  /**\n   * Creates a lookup span.\n   *\n   * @param key The cache key.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @return The span.\n   */\n  public static SimpleCacheSpan createLookup(String key, long position) {\n    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);\n  }\n\n  /**\n   * Creates an open hole span.\n   *\n   * @param key The cache key.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @return The span.\n   */\n  public static SimpleCacheSpan createOpenHole(String key, long position) {\n    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);\n  }\n\n  /**\n   * Creates a closed hole span.\n   *\n   * @param key The cache key.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @param length The length of the {@link CacheSpan}.\n   * @return The span.\n   */\n  public static SimpleCacheSpan createClosedHole(String key, long position, long length) {\n    return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null);\n  }\n\n  /**\n   * Creates a cache span from an underlying cache file. Upgrades the file if necessary.\n   *\n   * @param file The cache file.\n   * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the\n   *     underlying file system. Querying the underlying file system can be expensive, so callers\n   *     that already know the length of the file should pass it explicitly.\n   * @return The span, or null if the file name is not correctly formatted, or if the id is not\n   *     present in the content index, or if the length is 0.\n   */\n  @Nullable\n  public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) {\n    return createCacheEntry(file, length, /* lastTouchTimestamp= */ C.TIME_UNSET, index);\n  }\n\n  /**\n   * Creates a cache span from an underlying cache file. Upgrades the file if necessary.\n   *\n   * @param file The cache file.\n   * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the\n   *     underlying file system. Querying the underlying file system can be expensive, so callers\n   *     that already know the length of the file should pass it explicitly.\n   * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file\n   *     timestamp.\n   * @return The span, or null if the file name is not correctly formatted, or if the id is not\n   *     present in the content index, or if the length is 0.\n   */\n  @Nullable\n  public static SimpleCacheSpan createCacheEntry(\n      File file, long length, long lastTouchTimestamp, CachedContentIndex index) {\n    String name = file.getName();\n    if (!name.endsWith(SUFFIX)) {\n      @Nullable File upgradedFile = upgradeFile(file, index);\n      if (upgradedFile == null) {\n        return null;\n      }\n      file = upgradedFile;\n      name = file.getName();\n    }\n\n    Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);\n    if (!matcher.matches()) {\n      return null;\n    }\n\n    int id = Integer.parseInt(matcher.group(1));\n    String key = index.getKeyForId(id);\n    if (key == null) {\n      return null;\n    }\n\n    if (length == C.LENGTH_UNSET) {\n      length = file.length();\n    }\n    if (length == 0) {\n      return null;\n    }\n\n    long position = Long.parseLong(matcher.group(2));\n    if (lastTouchTimestamp == C.TIME_UNSET) {\n      lastTouchTimestamp = Long.parseLong(matcher.group(3));\n    }\n    return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file);\n  }\n\n  /**\n   * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}.\n   *\n   * @param file The cache file.\n   * @param index Cached content index.\n   * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the\n   *     file can not be renamed.\n   */\n  @Nullable\n  private static File upgradeFile(File file, CachedContentIndex index) {\n    String key;\n    String filename = file.getName();\n    Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);\n    if (matcher.matches()) {\n      key = Util.unescapeFileName(matcher.group(1));\n      if (key == null) {\n        return null;\n      }\n    } else {\n      matcher = CACHE_FILE_PATTERN_V1.matcher(filename);\n      if (!matcher.matches()) {\n        return null;\n      }\n      key = matcher.group(1); // Keys were not escaped in version 1.\n    }\n\n    File newCacheFile =\n        getCacheFile(\n            Assertions.checkStateNotNull(file.getParentFile()),\n            index.assignIdForKey(key),\n            Long.parseLong(matcher.group(2)),\n            Long.parseLong(matcher.group(3)));\n    if (!file.renameTo(newCacheFile)) {\n      return null;\n    }\n    return newCacheFile;\n  }\n\n  /**\n   * @param key The cache key.\n   * @param position The position of the {@link CacheSpan} in the original stream.\n   * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an\n   *     open-ended hole.\n   * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link\n   *     #isCached} is false.\n   * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.\n   */\n  private SimpleCacheSpan(\n      String key, long position, long length, long lastTouchTimestamp, @Nullable File file) {\n    super(key, position, length, lastTouchTimestamp, file);\n  }\n\n  /**\n   * Returns a copy of this CacheSpan with a new file and last touch timestamp.\n   *\n   * @param file The new file.\n   * @param lastTouchTimestamp The new last touch time.\n   * @return A copy with the new file and last touch timestamp.\n   * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false).\n   */\n  public SimpleCacheSpan copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) {\n    Assertions.checkState(isCached);\n    return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.crypto;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.IOException;\nimport javax.crypto.Cipher;\n\n/**\n * A wrapping {@link DataSink} that encrypts the data being consumed.\n */\npublic final class AesCipherDataSink implements DataSink {\n\n  private final DataSink wrappedDataSink;\n  private final byte[] secretKey;\n  @Nullable private final byte[] scratch;\n\n  @Nullable private AesFlushingCipher cipher;\n\n  /**\n   * Create an instance whose {@code write} methods have the side effect of overwriting the input\n   * {@code data}. Use this constructor for maximum efficiency in the case that there is no\n   * requirement for the input data arrays to remain unchanged.\n   *\n   * @param secretKey The key data.\n   * @param wrappedDataSink The wrapped {@link DataSink}.\n   */\n  public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) {\n    this(secretKey, wrappedDataSink, null);\n  }\n\n  /**\n   * Create an instance whose {@code write} methods are free of side effects. Use this constructor\n   * when the input data arrays are required to remain unchanged.\n   *\n   * @param secretKey The key data.\n   * @param wrappedDataSink The wrapped {@link DataSink}.\n   * @param scratch Scratch space. Data is encrypted into this array before being written to the\n   *     wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a\n   *     write is larger than the size of this array the write will still succeed, but multiple\n   *     cipher calls will be required to complete the operation. If {@code null} then encryption\n   *     will overwrite the input {@code data}.\n   */\n  public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) {\n    this.wrappedDataSink = wrappedDataSink;\n    this.secretKey = secretKey;\n    this.scratch = scratch;\n  }\n\n  @Override\n  public void open(DataSpec dataSpec) throws IOException {\n    wrappedDataSink.open(dataSpec);\n    long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);\n    cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,\n        dataSpec.absoluteStreamPosition);\n  }\n\n  @Override\n  public void write(byte[] data, int offset, int length) throws IOException {\n    if (scratch == null) {\n      // In-place mode. Writes over the input data.\n      castNonNull(cipher).updateInPlace(data, offset, length);\n      wrappedDataSink.write(data, offset, length);\n    } else {\n      // Use scratch space. The original data remains intact.\n      int bytesProcessed = 0;\n      while (bytesProcessed < length) {\n        int bytesToProcess = Math.min(length - bytesProcessed, scratch.length);\n        castNonNull(cipher)\n            .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0);\n        wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess);\n        bytesProcessed += bytesToProcess;\n      }\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    cipher = null;\n    wrappedDataSink.close();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.crypto;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport javax.crypto.Cipher;\n\n/**\n * A {@link DataSource} that decrypts the data read from an upstream source.\n */\npublic final class AesCipherDataSource implements DataSource {\n\n  private final DataSource upstream;\n  private final byte[] secretKey;\n\n  @Nullable private AesFlushingCipher cipher;\n\n  public AesCipherDataSource(byte[] secretKey, DataSource upstream) {\n    this.upstream = upstream;\n    this.secretKey = secretKey;\n  }\n\n  @Override\n  public void addTransferListener(TransferListener transferListener) {\n    upstream.addTransferListener(transferListener);\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws IOException {\n    long dataLength = upstream.open(dataSpec);\n    long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);\n    cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce,\n        dataSpec.absoluteStreamPosition);\n    return dataLength;\n  }\n\n  @Override\n  public int read(byte[] data, int offset, int readLength) throws IOException {\n    if (readLength == 0) {\n      return 0;\n    }\n    int read = upstream.read(data, offset, readLength);\n    if (read == C.RESULT_END_OF_INPUT) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    castNonNull(cipher).updateInPlace(data, offset, read);\n    return read;\n  }\n\n  @Override\n  @Nullable\n  public Uri getUri() {\n    return upstream.getUri();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return upstream.getResponseHeaders();\n  }\n\n  @Override\n  public void close() throws IOException {\n    cipher = null;\n    upstream.close();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.crypto;\n\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport javax.crypto.Cipher;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.ShortBufferException;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\n\n/**\n * A flushing variant of a AES/CTR/NoPadding {@link Cipher}.\n *\n * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all\n * of the bytes input (and hence output the same number of bytes).\n */\npublic final class AesFlushingCipher {\n\n  private final Cipher cipher;\n  private final int blockSize;\n  private final byte[] zerosBlock;\n  private final byte[] flushedBlock;\n\n  private int pendingXorBytes;\n\n  public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) {\n    try {\n      cipher = Cipher.getInstance(\"AES/CTR/NoPadding\");\n      blockSize = cipher.getBlockSize();\n      zerosBlock = new byte[blockSize];\n      flushedBlock = new byte[blockSize];\n      long counter = offset / blockSize;\n      int startPadding = (int) (offset % blockSize);\n      cipher.init(\n          mode,\n          new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), \"/\")[0]),\n          new IvParameterSpec(getInitializationVector(nonce, counter)));\n      if (startPadding != 0) {\n        updateInPlace(new byte[startPadding], 0, startPadding);\n      }\n    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException\n        | InvalidAlgorithmParameterException e) {\n      // Should never happen.\n      throw new RuntimeException(e);\n    }\n  }\n\n  public void updateInPlace(byte[] data, int offset, int length) {\n    update(data, offset, length, data, offset);\n  }\n\n  public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) {\n    // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need\n    // to manually transform the data that actually ended the block. See the comment below for more\n    // details.\n    while (pendingXorBytes > 0) {\n      out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]);\n      outOffset++;\n      inOffset++;\n      pendingXorBytes--;\n      length--;\n      if (length == 0) {\n        return;\n      }\n    }\n\n    // Do the bulk of the update.\n    int written = nonFlushingUpdate(in, inOffset, length, out, outOffset);\n    if (length == written) {\n      return;\n    }\n\n    // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,\n    // so that the corresponding bytes output by the cipher are those that would have been XORed\n    // against the real end-of-block data to transform it. We store these bytes so that we can\n    // perform the transformation manually in the case of a subsequent call to this method with\n    // the real data.\n    int bytesToFlush = length - written;\n    Assertions.checkState(bytesToFlush < blockSize);\n    outOffset += written;\n    pendingXorBytes = blockSize - bytesToFlush;\n    written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0);\n    Assertions.checkState(written == blockSize);\n    // The first part of xorBytes contains the flushed data, which we copy out. The remainder\n    // contains the bytes that will be needed for manual transformation in a subsequent call.\n    for (int i = 0; i < bytesToFlush; i++) {\n      out[outOffset++] = flushedBlock[i];\n    }\n  }\n\n  private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) {\n    try {\n      return cipher.update(in, inOffset, length, out, outOffset);\n    } catch (ShortBufferException e) {\n      // Should never happen.\n      throw new RuntimeException(e);\n    }\n  }\n\n  private byte[] getInitializationVector(long nonce, long counter) {\n    return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.upstream.crypto;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Utility functions for the crypto package.\n */\n/* package */ final class CryptoUtil {\n\n  private CryptoUtil() {}\n\n  /**\n   * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash\n   * values produced by this function are less likely to collide than those produced by {@link\n   * #hashCode()}.\n   */\n  public static long getFNV64Hash(@Nullable String input) {\n    if (input == null) {\n      return 0;\n    }\n\n    long hash = 0;\n    for (int i = 0; i < input.length(); i++) {\n      hash ^= input.charAt(i);\n      // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).\n      hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40);\n    }\n    return hash;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/Assertions.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\n\n/**\n * Provides methods for asserting the truth of expressions and properties.\n */\npublic final class Assertions {\n\n  private Assertions() {}\n\n  /**\n   * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.\n   *\n   * @param expression The expression to evaluate.\n   * @throws IllegalArgumentException If {@code expression} is false.\n   */\n  public static void checkArgument(boolean expression) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {\n      throw new IllegalArgumentException();\n    }\n  }\n\n  /**\n   * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.\n   *\n   * @param expression The expression to evaluate.\n   * @param errorMessage The exception message if an exception is thrown. The message is converted\n   *     to a {@link String} using {@link String#valueOf(Object)}.\n   * @throws IllegalArgumentException If {@code expression} is false.\n   */\n  public static void checkArgument(boolean expression, Object errorMessage) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {\n      throw new IllegalArgumentException(String.valueOf(errorMessage));\n    }\n  }\n\n  /**\n   * Throws {@link IndexOutOfBoundsException} if {@code index} falls outside the specified bounds.\n   *\n   * @param index The index to test.\n   * @param start The start of the allowed range (inclusive).\n   * @param limit The end of the allowed range (exclusive).\n   * @return The {@code index} that was validated.\n   * @throws IndexOutOfBoundsException If {@code index} falls outside the specified bounds.\n   */\n  public static int checkIndex(int index, int start, int limit) {\n    if (index < start || index >= limit) {\n      throw new IndexOutOfBoundsException();\n    }\n    return index;\n  }\n\n  /**\n   * Throws {@link IllegalStateException} if {@code expression} evaluates to false.\n   *\n   * @param expression The expression to evaluate.\n   * @throws IllegalStateException If {@code expression} is false.\n   */\n  public static void checkState(boolean expression) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {\n      throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Throws {@link IllegalStateException} if {@code expression} evaluates to false.\n   *\n   * @param expression The expression to evaluate.\n   * @param errorMessage The exception message if an exception is thrown. The message is converted\n   *     to a {@link String} using {@link String#valueOf(Object)}.\n   * @throws IllegalStateException If {@code expression} is false.\n   */\n  public static void checkState(boolean expression, Object errorMessage) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {\n      throw new IllegalStateException(String.valueOf(errorMessage));\n    }\n  }\n\n  /**\n   * Throws {@link IllegalStateException} if {@code reference} is null.\n   *\n   * @param <T> The type of the reference.\n   * @param reference The reference.\n   * @return The non-null reference that was validated.\n   * @throws IllegalStateException If {@code reference} is null.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static <T> T checkStateNotNull(@Nullable T reference) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {\n      throw new IllegalStateException();\n    }\n    return reference;\n  }\n\n  /**\n   * Throws {@link IllegalStateException} if {@code reference} is null.\n   *\n   * @param <T> The type of the reference.\n   * @param reference The reference.\n   * @param errorMessage The exception message to use if the check fails. The message is converted\n   *     to a string using {@link String#valueOf(Object)}.\n   * @return The non-null reference that was validated.\n   * @throws IllegalStateException If {@code reference} is null.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static <T> T checkStateNotNull(@Nullable T reference, Object errorMessage) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {\n      throw new IllegalStateException(String.valueOf(errorMessage));\n    }\n    return reference;\n  }\n\n  /**\n   * Throws {@link NullPointerException} if {@code reference} is null.\n   *\n   * @param <T> The type of the reference.\n   * @param reference The reference.\n   * @return The non-null reference that was validated.\n   * @throws NullPointerException If {@code reference} is null.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static <T> T checkNotNull(@Nullable T reference) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {\n      throw new NullPointerException();\n    }\n    return reference;\n  }\n\n  /**\n   * Throws {@link NullPointerException} if {@code reference} is null.\n   *\n   * @param <T> The type of the reference.\n   * @param reference The reference.\n   * @param errorMessage The exception message to use if the check fails. The message is converted\n   *     to a string using {@link String#valueOf(Object)}.\n   * @return The non-null reference that was validated.\n   * @throws NullPointerException If {@code reference} is null.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static <T> T checkNotNull(@Nullable T reference, Object errorMessage) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {\n      throw new NullPointerException(String.valueOf(errorMessage));\n    }\n    return reference;\n  }\n\n  /**\n   * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.\n   *\n   * @param string The string to check.\n   * @return The non-null, non-empty string that was validated.\n   * @throws IllegalArgumentException If {@code string} is null or 0-length.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static String checkNotEmpty(@Nullable String string) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {\n      throw new IllegalArgumentException();\n    }\n    return string;\n  }\n\n  /**\n   * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.\n   *\n   * @param string The string to check.\n   * @param errorMessage The exception message to use if the check fails. The message is converted\n   *     to a string using {@link String#valueOf(Object)}.\n   * @return The non-null, non-empty string that was validated.\n   * @throws IllegalArgumentException If {@code string} is null or 0-length.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull({\"#1\"})\n  public static String checkNotEmpty(@Nullable String string, Object errorMessage) {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {\n      throw new IllegalArgumentException(String.valueOf(errorMessage));\n    }\n    return string;\n  }\n\n  /**\n   * Throws {@link IllegalStateException} if the calling thread is not the application's main\n   * thread.\n   *\n   * @throws IllegalStateException If the calling thread is not the application's main thread.\n   */\n  public static void checkMainThread() {\n    if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {\n      throw new IllegalStateException(\"Not in applications main thread\");\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * A helper class for performing atomic operations on a file by creating a backup file until a write\n * has successfully completed.\n *\n * <p>Atomic file guarantees file integrity by ensuring that a file has been completely written and\n * synced to disk before removing its backup. As long as the backup file exists, the original file\n * is considered to be invalid (left over from a previous attempt to write the file).\n *\n * <p>Atomic file does not confer any file locking semantics. Do not use this class when the file\n * may be accessed or modified concurrently by multiple threads or processes. The caller is\n * responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.\n */\npublic final class AtomicFile {\n\n  private static final String TAG = \"AtomicFile\";\n\n  private final File baseName;\n  private final File backupName;\n\n  /**\n   * Create a new AtomicFile for a file located at the given File path. The secondary backup file\n   * will be the same file path with \".bak\" appended.\n   */\n  public AtomicFile(File baseName) {\n    this.baseName = baseName;\n    backupName = new File(baseName.getPath() + \".bak\");\n  }\n\n  /** Returns whether the file or its backup exists. */\n  public boolean exists() {\n    return baseName.exists() || backupName.exists();\n  }\n\n  /** Delete the atomic file. This deletes both the base and backup files. */\n  public void delete() {\n    baseName.delete();\n    backupName.delete();\n  }\n\n  /**\n   * Start a new write operation on the file. This returns an {@link OutputStream} to which you can\n   * write the new file data. If the whole data is written successfully you <em>must</em> call\n   * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()}\n   * only to free up resources used by it.\n   *\n   * <p>Example usage:\n   *\n   * <pre>\n   *   DataOutputStream dataOutput = null;\n   *   try {\n   *     OutputStream outputStream = atomicFile.startWrite();\n   *     dataOutput = new DataOutputStream(outputStream); // Wrapper stream\n   *     dataOutput.write(data1);\n   *     dataOutput.write(data2);\n   *     atomicFile.endWrite(dataOutput); // Pass wrapper stream\n   *   } finally{\n   *     if (dataOutput != null) {\n   *       dataOutput.close();\n   *     }\n   *   }\n   * </pre>\n   *\n   * <p>Note that if another thread is currently performing a write, this will simply replace\n   * whatever that thread is writing with the new file being written by this thread, and when the\n   * other thread finishes the write the new write operation will no longer be safe (or will be\n   * lost). You must do your own threading protection for access to AtomicFile.\n   */\n  public OutputStream startWrite() throws IOException {\n    // Rename the current file so it may be used as a backup during the next read\n    if (baseName.exists()) {\n      if (!backupName.exists()) {\n        if (!baseName.renameTo(backupName)) {\n          Log.w(TAG, \"Couldn't rename file \" + baseName + \" to backup file \" + backupName);\n        }\n      } else {\n        baseName.delete();\n      }\n    }\n    OutputStream str;\n    try {\n      str = new AtomicFileOutputStream(baseName);\n    } catch (FileNotFoundException e) {\n      File parent = baseName.getParentFile();\n      if (parent == null || !parent.mkdirs()) {\n        throw new IOException(\"Couldn't create \" + baseName, e);\n      }\n      // Try again now that we've created the parent directory.\n      try {\n        str = new AtomicFileOutputStream(baseName);\n      } catch (FileNotFoundException e2) {\n        throw new IOException(\"Couldn't create \" + baseName, e2);\n      }\n    }\n    return str;\n  }\n\n  /**\n   * Call when you have successfully finished writing to the stream returned by {@link\n   * #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the\n   * atomic file will return the new file stream.\n   *\n   * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link\n   *     #startWrite()}.\n   * @see #startWrite()\n   */\n  public void endWrite(OutputStream str) throws IOException {\n    str.close();\n    // If close() throws exception, the next line is skipped.\n    backupName.delete();\n  }\n\n  /**\n   * Open the atomic file for reading. If there previously was an incomplete write, this will roll\n   * back to the last good data before opening for read.\n   *\n   * <p>Note that if another thread is currently performing a write, this will incorrectly consider\n   * it to be in the state of a bad write and roll back, causing the new data currently being\n   * written to be dropped. You must do your own threading protection for access to AtomicFile.\n   */\n  public InputStream openRead() throws FileNotFoundException {\n    restoreBackup();\n    return new FileInputStream(baseName);\n  }\n\n  private void restoreBackup() {\n    if (backupName.exists()) {\n      baseName.delete();\n      backupName.renameTo(baseName);\n    }\n  }\n\n  private static final class AtomicFileOutputStream extends OutputStream {\n\n    private final FileOutputStream fileOutputStream;\n    private boolean closed = false;\n\n    public AtomicFileOutputStream(File file) throws FileNotFoundException {\n      fileOutputStream = new FileOutputStream(file);\n    }\n\n    @Override\n    public void close() throws IOException {\n      if (closed) {\n        return;\n      }\n      closed = true;\n      flush();\n      try {\n        fileOutputStream.getFD().sync();\n      } catch (IOException e) {\n        Log.w(TAG, \"Failed to sync file descriptor:\", e);\n      }\n      fileOutputStream.close();\n    }\n\n    @Override\n    public void flush() throws IOException {\n      fileOutputStream.flush();\n    }\n\n    @Override\n    public void write(int b) throws IOException {\n      fileOutputStream.write(b);\n    }\n\n    @Override\n    public void write(byte[] b) throws IOException {\n      fileOutputStream.write(b);\n    }\n\n    @Override\n    public void write(byte[] b, int off, int len) throws IOException {\n      fileOutputStream.write(b, off, len);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/Clock.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\n\n/**\n * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The\n * {@link #DEFAULT} implementation must be used for all non-test cases.\n */\npublic interface Clock {\n\n  /**\n   * Default {@link Clock} to use for all non-test cases.\n   */\n  Clock DEFAULT = new SystemClock();\n\n  /** @see android.os.SystemClock#elapsedRealtime() */\n  long elapsedRealtime();\n\n  /** @see android.os.SystemClock#uptimeMillis() */\n  long uptimeMillis();\n\n  /** @see android.os.SystemClock#sleep(long) */\n  void sleep(long sleepTimeMs);\n\n  /**\n   * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling\n   * messages.\n   *\n   * @see Handler#Handler(Looper, Handler.Callback)\n   */\n  HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.util.Pair;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Provides static utility methods for manipulating various types of codec specific data.\n */\npublic final class CodecSpecificDataUtil {\n\n  private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};\n\n  private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF;\n\n  private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {\n    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350\n  };\n\n  private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1;\n  /**\n   * In the channel configurations below, <A> indicates a single channel element; (A, B) indicates a\n   * channel pair element; and [A] indicates a low-frequency effects element.\n   * The speaker mapping short forms used are:\n   * - FC: front center\n   * - BC: back center\n   * - FL/FR: front left/right\n   * - FCL/FCR: front center left/right\n   * - FTL/FTR: front top left/right\n   * - SL/SR: back surround left/right\n   * - BL/BR: back left/right\n   * - LFE: low frequency effects\n   */\n  private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE =\n      new int[] {\n        0,\n        1, /* mono: <FC> */\n        2, /* stereo: (FL, FR) */\n        3, /* 3.0: <FC>, (FL, FR) */\n        4, /* 4.0: <FC>, (FL, FR), <BC> */\n        5, /* 5.0 back: <FC>, (FL, FR), (SL, SR) */\n        6, /* 5.1 back: <FC>, (FL, FR), (SL, SR), <BC>, [LFE] */\n        8, /* 7.1 wide back: <FC>, (FCL, FCR), (FL, FR), (SL, SR), [LFE] */\n        AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,\n        AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,\n        AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,\n        7, /* 6.1: <FC>, (FL, FR), (SL, SR), <RC>, [LFE] */\n        8, /* 7.1: <FC>, (FL, FR), (SL, SR), (BL, BR), [LFE] */\n        AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,\n        8, /* 7.1 top: <FC>, (FL, FR), (SL, SR), [LFE], (FTL, FTR) */\n        AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID\n      };\n\n  // Advanced Audio Coding Low-Complexity profile.\n  private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;\n  // Spectral Band Replication.\n  private static final int AUDIO_OBJECT_TYPE_SBR = 5;\n  // Error Resilient Bit-Sliced Arithmetic Coding.\n  private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22;\n  // Parametric Stereo.\n  private static final int AUDIO_OBJECT_TYPE_PS = 29;\n  // Escape code for extended audio object types.\n  private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31;\n\n  private CodecSpecificDataUtil() {}\n\n  /**\n   * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1\n   *\n   * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.\n   * @return A pair consisting of the sample rate in Hz and the channel count.\n   * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.\n   */\n  public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig)\n      throws ParserException {\n    return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false);\n  }\n\n  /**\n   * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1\n   *\n   * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The\n   *     position is advanced to the end of the AudioSpecificConfig.\n   * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for\n   *     knowing the length of the configuration payload.\n   * @return A pair consisting of the sample rate in Hz and the channel count.\n   * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.\n   */\n  public static Pair<Integer, Integer> parseAacAudioSpecificConfig(\n      ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException {\n    int audioObjectType = getAacAudioObjectType(bitArray);\n    int sampleRate = getAacSamplingFrequency(bitArray);\n    int channelConfiguration = bitArray.readBits(4);\n    if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) {\n      // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with\n      // explicit signaling, we return the extension sampling frequency as the sample rate of the\n      // content; this is identical to the sample rate of the decoded output but may differ from\n      // the sample rate set above.\n      // Use the extensionSamplingFrequencyIndex.\n      sampleRate = getAacSamplingFrequency(bitArray);\n      audioObjectType = getAacAudioObjectType(bitArray);\n      if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) {\n        // Use the extensionChannelConfiguration.\n        channelConfiguration = bitArray.readBits(4);\n      }\n    }\n\n    if (forceReadToEnd) {\n      switch (audioObjectType) {\n        case 1:\n        case 2:\n        case 3:\n        case 4:\n        case 6:\n        case 7:\n        case 17:\n        case 19:\n        case 20:\n        case 21:\n        case 22:\n        case 23:\n          parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration);\n          break;\n        default:\n          throw new ParserException(\"Unsupported audio object type: \" + audioObjectType);\n      }\n      switch (audioObjectType) {\n        case 17:\n        case 19:\n        case 20:\n        case 21:\n        case 22:\n        case 23:\n          int epConfig = bitArray.readBits(2);\n          if (epConfig == 2 || epConfig == 3) {\n            throw new ParserException(\"Unsupported epConfig: \" + epConfig);\n          }\n          break;\n      }\n    }\n    // For supported containers, bits_to_decode() is always 0.\n    int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];\n    Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);\n    return Pair.create(sampleRate, channelCount);\n  }\n\n  /**\n   * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1\n   *\n   * @param sampleRate The sample rate in Hz.\n   * @param channelCount The channel count.\n   * @return The AudioSpecificConfig.\n   */\n  public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int channelCount) {\n    int sampleRateIndex = C.INDEX_UNSET;\n    for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) {\n      if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) {\n        sampleRateIndex = i;\n      }\n    }\n    int channelConfig = C.INDEX_UNSET;\n    for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {\n      if (channelCount == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {\n        channelConfig = i;\n      }\n    }\n    if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) {\n      throw new IllegalArgumentException(\n          \"Invalid sample rate or number of channels: \" + sampleRate + \", \" + channelCount);\n    }\n    return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig);\n  }\n\n  /**\n   * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1\n   *\n   * @param audioObjectType The audio object type.\n   * @param sampleRateIndex The sample rate index.\n   * @param channelConfig The channel configuration.\n   * @return The AudioSpecificConfig.\n   */\n  public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex,\n      int channelConfig) {\n    byte[] specificConfig = new byte[2];\n    specificConfig[0] = (byte) (((audioObjectType << 3) & 0xF8) | ((sampleRateIndex >> 1) & 0x07));\n    specificConfig[1] = (byte) (((sampleRateIndex << 7) & 0x80) | ((channelConfig << 3) & 0x78));\n    return specificConfig;\n  }\n\n  /**\n   * Parses an ALAC AudioSpecificConfig (i.e. an <a\n   * href=\"https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt\">ALACSpecificConfig</a>).\n   *\n   * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.\n   * @return A pair consisting of the sample rate in Hz and the channel count.\n   */\n  public static Pair<Integer, Integer> parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) {\n    ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig);\n    byteArray.setPosition(9);\n    int channelCount = byteArray.readUnsignedByte();\n    byteArray.setPosition(20);\n    int sampleRate = byteArray.readUnsignedIntToInt();\n    return Pair.create(sampleRate, channelCount);\n  }\n\n  /**\n   * Builds an RFC 6381 AVC codec string using the provided parameters.\n   *\n   * @param profileIdc The encoding profile.\n   * @param constraintsFlagsAndReservedZero2Bits The constraint flags followed by the reserved zero\n   *     2 bits, all contained in the least significant byte of the integer.\n   * @param levelIdc The encoding level.\n   * @return An RFC 6381 AVC codec string built using the provided parameters.\n   */\n  public static String buildAvcCodecString(\n      int profileIdc, int constraintsFlagsAndReservedZero2Bits, int levelIdc) {\n    return String.format(\n        \"avc1.%02X%02X%02X\", profileIdc, constraintsFlagsAndReservedZero2Bits, levelIdc);\n  }\n\n  /**\n   * Constructs a NAL unit consisting of the NAL start code followed by the specified data.\n   *\n   * @param data An array containing the data that should follow the NAL start code.\n   * @param offset The start offset into {@code data}.\n   * @param length The number of bytes to copy from {@code data}\n   * @return The constructed NAL unit.\n   */\n  public static byte[] buildNalUnit(byte[] data, int offset, int length) {\n    byte[] nalUnit = new byte[length + NAL_START_CODE.length];\n    System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);\n    System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);\n    return nalUnit;\n  }\n\n  /**\n   * Splits an array of NAL units.\n   *\n   * <p>If the input consists of NAL start code delimited units, then the returned array consists of\n   * the split NAL units, each of which is still prefixed with the NAL start code. For any other\n   * input, null is returned.\n   *\n   * @param data An array of data.\n   * @return The individual NAL units, or null if the input did not consist of NAL start code\n   *     delimited units.\n   */\n  public static @Nullable byte[][] splitNalUnits(byte[] data) {\n    if (!isNalStartCode(data, 0)) {\n      // data does not consist of NAL start code delimited units.\n      return null;\n    }\n    List<Integer> starts = new ArrayList<>();\n    int nalUnitIndex = 0;\n    do {\n      starts.add(nalUnitIndex);\n      nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);\n    } while (nalUnitIndex != C.INDEX_UNSET);\n    byte[][] split = new byte[starts.size()][];\n    for (int i = 0; i < starts.size(); i++) {\n      int startIndex = starts.get(i);\n      int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;\n      byte[] nal = new byte[endIndex - startIndex];\n      System.arraycopy(data, startIndex, nal, 0, nal.length);\n      split[i] = nal;\n    }\n    return split;\n  }\n\n  /**\n   * Finds the next occurrence of the NAL start code from a given index.\n   *\n   * @param data The data in which to search.\n   * @param index The first index to test.\n   * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}.\n   */\n  private static int findNalStartCode(byte[] data, int index) {\n    int endIndex = data.length - NAL_START_CODE.length;\n    for (int i = index; i <= endIndex; i++) {\n      if (isNalStartCode(data, i)) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /**\n   * Tests whether there exists a NAL start code at a given index.\n   *\n   * @param data The data.\n   * @param index The index to test.\n   * @return Whether there exists a start code that begins at {@code index}.\n   */\n  private static boolean isNalStartCode(byte[] data, int index) {\n    if (data.length - index <= NAL_START_CODE.length) {\n      return false;\n    }\n    for (int j = 0; j < NAL_START_CODE.length; j++) {\n      if (data[index + j] != NAL_START_CODE[j]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14.\n   *\n   * @param bitArray The bit array containing the audio specific configuration.\n   * @return The audio object type.\n   */\n  private static int getAacAudioObjectType(ParsableBitArray bitArray) {\n    int audioObjectType = bitArray.readBits(5);\n    if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) {\n      audioObjectType = 32 + bitArray.readBits(6);\n    }\n    return audioObjectType;\n  }\n\n  /**\n   * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3\n   * (2005) Table 1.13.\n   *\n   * @param bitArray The bit array containing the audio specific configuration.\n   * @return The sampling frequency.\n   */\n  private static int getAacSamplingFrequency(ParsableBitArray bitArray) {\n    int samplingFrequency;\n    int frequencyIndex = bitArray.readBits(4);\n    if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {\n      samplingFrequency = bitArray.readBits(24);\n    } else {\n      Assertions.checkArgument(frequencyIndex < 13);\n      samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];\n    }\n    return samplingFrequency;\n  }\n\n  private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType,\n      int channelConfiguration) {\n    bitArray.skipBits(1); // frameLengthFlag.\n    boolean dependsOnCoreDecoder = bitArray.readBit();\n    if (dependsOnCoreDecoder) {\n      bitArray.skipBits(14); // coreCoderDelay.\n    }\n    boolean extensionFlag = bitArray.readBit();\n    if (channelConfiguration == 0) {\n      throw new UnsupportedOperationException(); // TODO: Implement programConfigElement();\n    }\n    if (audioObjectType == 6 || audioObjectType == 20) {\n      bitArray.skipBits(3); // layerNr.\n    }\n    if (extensionFlag) {\n      if (audioObjectType == 22) {\n        bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11).\n      }\n      if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20\n          || audioObjectType == 23) {\n        // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag,\n        // aacSpectralDataResilienceFlag.\n        bitArray.skipBits(3);\n      }\n      bitArray.skipBits(1); // extensionFlag3.\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ColorParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.text.TextUtils;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Parser for color expressions found in styling formats, e.g. TTML and CSS.\n *\n * @see <a href=\"https://w3c.github.io/webvtt/#styling\">WebVTT CSS Styling</a>\n * @see <a href=\"https://www.w3.org/TR/ttml2/\">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>\n */\npublic final class ColorParser {\n\n  private static final String RGB = \"rgb\";\n  private static final String RGBA = \"rgba\";\n\n  private static final Pattern RGB_PATTERN = Pattern.compile(\n      \"^rgb\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3})\\\\)$\");\n\n  private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile(\n      \"^rgba\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3})\\\\)$\");\n\n  private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile(\n      \"^rgba\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3}),(\\\\d*\\\\.?\\\\d*?)\\\\)$\");\n\n  private static final Map<String, Integer> COLOR_MAP;\n\n  /**\n   * Parses a TTML color expression.\n   *\n   * @param colorExpression The color expression.\n   * @return The parsed ARGB color.\n   */\n  public static int parseTtmlColor(String colorExpression) {\n    return parseColorInternal(colorExpression, false);\n  }\n\n  /**\n   * Parses a CSS color expression.\n   *\n   * @param colorExpression The color expression.\n   * @return The parsed ARGB color.\n   */\n  public static int parseCssColor(String colorExpression) {\n    return parseColorInternal(colorExpression, true);\n  }\n\n  private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {\n    Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));\n    colorExpression = colorExpression.replace(\" \", \"\");\n    if (colorExpression.charAt(0) == '#') {\n      // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.\n      int color = (int) Long.parseLong(colorExpression.substring(1), 16);\n      if (colorExpression.length() == 7) {\n        // Set the alpha value\n        color |= 0xFF000000;\n      } else if (colorExpression.length() == 9) {\n        // We have #RRGGBBAA, but we need #AARRGGBB\n        color = ((color & 0xFF) << 24) | (color >>> 8);\n      } else {\n        throw new IllegalArgumentException();\n      }\n      return color;\n    } else if (colorExpression.startsWith(RGBA)) {\n      Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA)\n          .matcher(colorExpression);\n      if (matcher.matches()) {\n        return argb(\n          alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4)))\n              : Integer.parseInt(matcher.group(4), 10),\n          Integer.parseInt(matcher.group(1), 10),\n          Integer.parseInt(matcher.group(2), 10),\n          Integer.parseInt(matcher.group(3), 10)\n        );\n      }\n    } else if (colorExpression.startsWith(RGB)) {\n      Matcher matcher = RGB_PATTERN.matcher(colorExpression);\n      if (matcher.matches()) {\n        return rgb(\n          Integer.parseInt(matcher.group(1), 10),\n          Integer.parseInt(matcher.group(2), 10),\n          Integer.parseInt(matcher.group(3), 10)\n        );\n      }\n    } else {\n      // we use our own color map\n      Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression));\n      if (color != null) {\n        return color;\n      }\n    }\n    throw new IllegalArgumentException();\n  }\n\n  private static int argb(int alpha, int red, int green, int blue) {\n    return (alpha << 24) | (red << 16) | (green << 8) | blue;\n  }\n\n  private static int rgb(int red, int green, int blue) {\n    return argb(0xFF, red, green, blue);\n  }\n\n  static {\n    COLOR_MAP = new HashMap<>();\n    COLOR_MAP.put(\"aliceblue\", 0xFFF0F8FF);\n    COLOR_MAP.put(\"antiquewhite\", 0xFFFAEBD7);\n    COLOR_MAP.put(\"aqua\", 0xFF00FFFF);\n    COLOR_MAP.put(\"aquamarine\", 0xFF7FFFD4);\n    COLOR_MAP.put(\"azure\", 0xFFF0FFFF);\n    COLOR_MAP.put(\"beige\", 0xFFF5F5DC);\n    COLOR_MAP.put(\"bisque\", 0xFFFFE4C4);\n    COLOR_MAP.put(\"black\", 0xFF000000);\n    COLOR_MAP.put(\"blanchedalmond\", 0xFFFFEBCD);\n    COLOR_MAP.put(\"blue\", 0xFF0000FF);\n    COLOR_MAP.put(\"blueviolet\", 0xFF8A2BE2);\n    COLOR_MAP.put(\"brown\", 0xFFA52A2A);\n    COLOR_MAP.put(\"burlywood\", 0xFFDEB887);\n    COLOR_MAP.put(\"cadetblue\", 0xFF5F9EA0);\n    COLOR_MAP.put(\"chartreuse\", 0xFF7FFF00);\n    COLOR_MAP.put(\"chocolate\", 0xFFD2691E);\n    COLOR_MAP.put(\"coral\", 0xFFFF7F50);\n    COLOR_MAP.put(\"cornflowerblue\", 0xFF6495ED);\n    COLOR_MAP.put(\"cornsilk\", 0xFFFFF8DC);\n    COLOR_MAP.put(\"crimson\", 0xFFDC143C);\n    COLOR_MAP.put(\"cyan\", 0xFF00FFFF);\n    COLOR_MAP.put(\"darkblue\", 0xFF00008B);\n    COLOR_MAP.put(\"darkcyan\", 0xFF008B8B);\n    COLOR_MAP.put(\"darkgoldenrod\", 0xFFB8860B);\n    COLOR_MAP.put(\"darkgray\", 0xFFA9A9A9);\n    COLOR_MAP.put(\"darkgreen\", 0xFF006400);\n    COLOR_MAP.put(\"darkgrey\", 0xFFA9A9A9);\n    COLOR_MAP.put(\"darkkhaki\", 0xFFBDB76B);\n    COLOR_MAP.put(\"darkmagenta\", 0xFF8B008B);\n    COLOR_MAP.put(\"darkolivegreen\", 0xFF556B2F);\n    COLOR_MAP.put(\"darkorange\", 0xFFFF8C00);\n    COLOR_MAP.put(\"darkorchid\", 0xFF9932CC);\n    COLOR_MAP.put(\"darkred\", 0xFF8B0000);\n    COLOR_MAP.put(\"darksalmon\", 0xFFE9967A);\n    COLOR_MAP.put(\"darkseagreen\", 0xFF8FBC8F);\n    COLOR_MAP.put(\"darkslateblue\", 0xFF483D8B);\n    COLOR_MAP.put(\"darkslategray\", 0xFF2F4F4F);\n    COLOR_MAP.put(\"darkslategrey\", 0xFF2F4F4F);\n    COLOR_MAP.put(\"darkturquoise\", 0xFF00CED1);\n    COLOR_MAP.put(\"darkviolet\", 0xFF9400D3);\n    COLOR_MAP.put(\"deeppink\", 0xFFFF1493);\n    COLOR_MAP.put(\"deepskyblue\", 0xFF00BFFF);\n    COLOR_MAP.put(\"dimgray\", 0xFF696969);\n    COLOR_MAP.put(\"dimgrey\", 0xFF696969);\n    COLOR_MAP.put(\"dodgerblue\", 0xFF1E90FF);\n    COLOR_MAP.put(\"firebrick\", 0xFFB22222);\n    COLOR_MAP.put(\"floralwhite\", 0xFFFFFAF0);\n    COLOR_MAP.put(\"forestgreen\", 0xFF228B22);\n    COLOR_MAP.put(\"fuchsia\", 0xFFFF00FF);\n    COLOR_MAP.put(\"gainsboro\", 0xFFDCDCDC);\n    COLOR_MAP.put(\"ghostwhite\", 0xFFF8F8FF);\n    COLOR_MAP.put(\"gold\", 0xFFFFD700);\n    COLOR_MAP.put(\"goldenrod\", 0xFFDAA520);\n    COLOR_MAP.put(\"gray\", 0xFF808080);\n    COLOR_MAP.put(\"green\", 0xFF008000);\n    COLOR_MAP.put(\"greenyellow\", 0xFFADFF2F);\n    COLOR_MAP.put(\"grey\", 0xFF808080);\n    COLOR_MAP.put(\"honeydew\", 0xFFF0FFF0);\n    COLOR_MAP.put(\"hotpink\", 0xFFFF69B4);\n    COLOR_MAP.put(\"indianred\", 0xFFCD5C5C);\n    COLOR_MAP.put(\"indigo\", 0xFF4B0082);\n    COLOR_MAP.put(\"ivory\", 0xFFFFFFF0);\n    COLOR_MAP.put(\"khaki\", 0xFFF0E68C);\n    COLOR_MAP.put(\"lavender\", 0xFFE6E6FA);\n    COLOR_MAP.put(\"lavenderblush\", 0xFFFFF0F5);\n    COLOR_MAP.put(\"lawngreen\", 0xFF7CFC00);\n    COLOR_MAP.put(\"lemonchiffon\", 0xFFFFFACD);\n    COLOR_MAP.put(\"lightblue\", 0xFFADD8E6);\n    COLOR_MAP.put(\"lightcoral\", 0xFFF08080);\n    COLOR_MAP.put(\"lightcyan\", 0xFFE0FFFF);\n    COLOR_MAP.put(\"lightgoldenrodyellow\", 0xFFFAFAD2);\n    COLOR_MAP.put(\"lightgray\", 0xFFD3D3D3);\n    COLOR_MAP.put(\"lightgreen\", 0xFF90EE90);\n    COLOR_MAP.put(\"lightgrey\", 0xFFD3D3D3);\n    COLOR_MAP.put(\"lightpink\", 0xFFFFB6C1);\n    COLOR_MAP.put(\"lightsalmon\", 0xFFFFA07A);\n    COLOR_MAP.put(\"lightseagreen\", 0xFF20B2AA);\n    COLOR_MAP.put(\"lightskyblue\", 0xFF87CEFA);\n    COLOR_MAP.put(\"lightslategray\", 0xFF778899);\n    COLOR_MAP.put(\"lightslategrey\", 0xFF778899);\n    COLOR_MAP.put(\"lightsteelblue\", 0xFFB0C4DE);\n    COLOR_MAP.put(\"lightyellow\", 0xFFFFFFE0);\n    COLOR_MAP.put(\"lime\", 0xFF00FF00);\n    COLOR_MAP.put(\"limegreen\", 0xFF32CD32);\n    COLOR_MAP.put(\"linen\", 0xFFFAF0E6);\n    COLOR_MAP.put(\"magenta\", 0xFFFF00FF);\n    COLOR_MAP.put(\"maroon\", 0xFF800000);\n    COLOR_MAP.put(\"mediumaquamarine\", 0xFF66CDAA);\n    COLOR_MAP.put(\"mediumblue\", 0xFF0000CD);\n    COLOR_MAP.put(\"mediumorchid\", 0xFFBA55D3);\n    COLOR_MAP.put(\"mediumpurple\", 0xFF9370DB);\n    COLOR_MAP.put(\"mediumseagreen\", 0xFF3CB371);\n    COLOR_MAP.put(\"mediumslateblue\", 0xFF7B68EE);\n    COLOR_MAP.put(\"mediumspringgreen\", 0xFF00FA9A);\n    COLOR_MAP.put(\"mediumturquoise\", 0xFF48D1CC);\n    COLOR_MAP.put(\"mediumvioletred\", 0xFFC71585);\n    COLOR_MAP.put(\"midnightblue\", 0xFF191970);\n    COLOR_MAP.put(\"mintcream\", 0xFFF5FFFA);\n    COLOR_MAP.put(\"mistyrose\", 0xFFFFE4E1);\n    COLOR_MAP.put(\"moccasin\", 0xFFFFE4B5);\n    COLOR_MAP.put(\"navajowhite\", 0xFFFFDEAD);\n    COLOR_MAP.put(\"navy\", 0xFF000080);\n    COLOR_MAP.put(\"oldlace\", 0xFFFDF5E6);\n    COLOR_MAP.put(\"olive\", 0xFF808000);\n    COLOR_MAP.put(\"olivedrab\", 0xFF6B8E23);\n    COLOR_MAP.put(\"orange\", 0xFFFFA500);\n    COLOR_MAP.put(\"orangered\", 0xFFFF4500);\n    COLOR_MAP.put(\"orchid\", 0xFFDA70D6);\n    COLOR_MAP.put(\"palegoldenrod\", 0xFFEEE8AA);\n    COLOR_MAP.put(\"palegreen\", 0xFF98FB98);\n    COLOR_MAP.put(\"paleturquoise\", 0xFFAFEEEE);\n    COLOR_MAP.put(\"palevioletred\", 0xFFDB7093);\n    COLOR_MAP.put(\"papayawhip\", 0xFFFFEFD5);\n    COLOR_MAP.put(\"peachpuff\", 0xFFFFDAB9);\n    COLOR_MAP.put(\"peru\", 0xFFCD853F);\n    COLOR_MAP.put(\"pink\", 0xFFFFC0CB);\n    COLOR_MAP.put(\"plum\", 0xFFDDA0DD);\n    COLOR_MAP.put(\"powderblue\", 0xFFB0E0E6);\n    COLOR_MAP.put(\"purple\", 0xFF800080);\n    COLOR_MAP.put(\"rebeccapurple\", 0xFF663399);\n    COLOR_MAP.put(\"red\", 0xFFFF0000);\n    COLOR_MAP.put(\"rosybrown\", 0xFFBC8F8F);\n    COLOR_MAP.put(\"royalblue\", 0xFF4169E1);\n    COLOR_MAP.put(\"saddlebrown\", 0xFF8B4513);\n    COLOR_MAP.put(\"salmon\", 0xFFFA8072);\n    COLOR_MAP.put(\"sandybrown\", 0xFFF4A460);\n    COLOR_MAP.put(\"seagreen\", 0xFF2E8B57);\n    COLOR_MAP.put(\"seashell\", 0xFFFFF5EE);\n    COLOR_MAP.put(\"sienna\", 0xFFA0522D);\n    COLOR_MAP.put(\"silver\", 0xFFC0C0C0);\n    COLOR_MAP.put(\"skyblue\", 0xFF87CEEB);\n    COLOR_MAP.put(\"slateblue\", 0xFF6A5ACD);\n    COLOR_MAP.put(\"slategray\", 0xFF708090);\n    COLOR_MAP.put(\"slategrey\", 0xFF708090);\n    COLOR_MAP.put(\"snow\", 0xFFFFFAFA);\n    COLOR_MAP.put(\"springgreen\", 0xFF00FF7F);\n    COLOR_MAP.put(\"steelblue\", 0xFF4682B4);\n    COLOR_MAP.put(\"tan\", 0xFFD2B48C);\n    COLOR_MAP.put(\"teal\", 0xFF008080);\n    COLOR_MAP.put(\"thistle\", 0xFFD8BFD8);\n    COLOR_MAP.put(\"tomato\", 0xFFFF6347);\n    COLOR_MAP.put(\"transparent\", 0x00000000);\n    COLOR_MAP.put(\"turquoise\", 0xFF40E0D0);\n    COLOR_MAP.put(\"violet\", 0xFFEE82EE);\n    COLOR_MAP.put(\"wheat\", 0xFFF5DEB3);\n    COLOR_MAP.put(\"white\", 0xFFFFFFFF);\n    COLOR_MAP.put(\"whitesmoke\", 0xFFF5F5F5);\n    COLOR_MAP.put(\"yellow\", 0xFFFFFF00);\n    COLOR_MAP.put(\"yellowgreen\", 0xFF9ACD32);\n  }\n\n  private ColorParser() {\n    // Prevent instantiation.\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\n/**\n * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return\n * whether they resulted in a change of state.\n */\npublic final class ConditionVariable {\n\n  private boolean isOpen;\n\n  /**\n   * Opens the condition and releases all threads that are blocked.\n   *\n   * @return True if the condition variable was opened. False if it was already open.\n   */\n  public synchronized boolean open() {\n    if (isOpen) {\n      return false;\n    }\n    isOpen = true;\n    notifyAll();\n    return true;\n  }\n\n  /**\n   * Closes the condition.\n   *\n   * @return True if the condition variable was closed. False if it was already closed.\n   */\n  public synchronized boolean close() {\n    boolean wasOpen = isOpen;\n    isOpen = false;\n    return wasOpen;\n  }\n\n  /**\n   * Blocks until the condition is opened.\n   *\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public synchronized void block() throws InterruptedException {\n    while (!isOpen) {\n      wait();\n    }\n  }\n\n  /**\n   * Blocks until the condition is opened or until {@code timeout} milliseconds have passed.\n   *\n   * @param timeout The maximum time to wait in milliseconds.\n   * @return True if the condition was opened, false if the call returns because of the timeout.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public synchronized boolean block(long timeout) throws InterruptedException {\n    long now = android.os.SystemClock.elapsedRealtime();\n    long end = now + timeout;\n    while (!isOpen && now < end) {\n      wait(end - now);\n      now = android.os.SystemClock.elapsedRealtime();\n    }\n    return isOpen;\n  }\n\n  /** Returns whether the condition is opened. */\n  public synchronized boolean isOpen() {\n    return isOpen;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.annotation.TargetApi;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGL14;\nimport android.opengl.EGLConfig;\nimport android.opengl.EGLContext;\nimport android.opengl.EGLDisplay;\nimport android.opengl.EGLSurface;\nimport android.opengl.GLES20;\nimport android.os.Handler;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */\n@TargetApi(17)\npublic final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable {\n\n  /** Listener to be called when the texture image on {@link SurfaceTexture} has been updated. */\n  public interface TextureImageListener {\n    /** Called when the {@link SurfaceTexture} receives a new frame from its image producer. */\n    void onFrameAvailable();\n  }\n\n  /**\n   * Secure mode to be used by the EGL surface and context. One of {@link #SECURE_MODE_NONE}, {@link\n   * #SECURE_MODE_SURFACELESS_CONTEXT} or {@link #SECURE_MODE_PROTECTED_PBUFFER}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})\n  public @interface SecureMode {}\n\n  /** No secure EGL surface and context required. */\n  public static final int SECURE_MODE_NONE = 0;\n  /** Creating a surfaceless, secured EGL context. */\n  public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;\n  /** Creating a secure surface backed by a pixel buffer. */\n  public static final int SECURE_MODE_PROTECTED_PBUFFER = 2;\n\n  private static final int EGL_SURFACE_WIDTH = 1;\n  private static final int EGL_SURFACE_HEIGHT = 1;\n\n  private static final int[] EGL_CONFIG_ATTRIBUTES =\n      new int[] {\n        EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,\n        EGL14.EGL_RED_SIZE, 8,\n        EGL14.EGL_GREEN_SIZE, 8,\n        EGL14.EGL_BLUE_SIZE, 8,\n        EGL14.EGL_ALPHA_SIZE, 8,\n        EGL14.EGL_DEPTH_SIZE, 0,\n        EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,\n        EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,\n        EGL14.EGL_NONE\n      };\n\n  private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;\n\n  /** A runtime exception to be thrown if some EGL operations failed. */\n  public static final class GlException extends RuntimeException {\n    private GlException(String msg) {\n      super(msg);\n    }\n  }\n\n  private final Handler handler;\n  private final int[] textureIdHolder;\n  @Nullable private final TextureImageListener callback;\n\n  @Nullable private EGLDisplay display;\n  @Nullable private EGLContext context;\n  @Nullable private EGLSurface surface;\n  @Nullable private SurfaceTexture texture;\n\n  /**\n   * @param handler The {@link Handler} that will be used to call {@link\n   *     SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that\n   *     {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s\n   *     looper.\n   */\n  public EGLSurfaceTexture(Handler handler) {\n    this(handler, /* callback= */ null);\n  }\n\n  /**\n   * @param handler The {@link Handler} that will be used to call {@link\n   *     SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that\n   *     {@link #init(int)} has to be called on the same looper thread as the looper of the {@link\n   *     Handler}.\n   * @param callback The {@link TextureImageListener} to be called when the texture image on {@link\n   *     SurfaceTexture} has been updated. This callback will be called on the same handler thread\n   *     as the {@code handler}.\n   */\n  public EGLSurfaceTexture(Handler handler, @Nullable TextureImageListener callback) {\n    this.handler = handler;\n    this.callback = callback;\n    textureIdHolder = new int[1];\n  }\n\n  /**\n   * Initializes required EGL parameters and creates the {@link SurfaceTexture}.\n   *\n   * @param secureMode The {@link SecureMode} to be used for EGL surface.\n   */\n  public void init(@SecureMode int secureMode) {\n    display = getDefaultDisplay();\n    EGLConfig config = chooseEGLConfig(display);\n    context = createEGLContext(display, config, secureMode);\n    surface = createEGLSurface(display, config, context, secureMode);\n    generateTextureIds(textureIdHolder);\n    texture = new SurfaceTexture(textureIdHolder[0]);\n    texture.setOnFrameAvailableListener(this);\n  }\n\n  /** Releases all allocated resources. */\n  @SuppressWarnings({\"nullness:argument.type.incompatible\"})\n  public void release() {\n    handler.removeCallbacks(this);\n    try {\n      if (texture != null) {\n        texture.release();\n        GLES20.glDeleteTextures(1, textureIdHolder, 0);\n      }\n    } finally {\n      if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) {\n        EGL14.eglMakeCurrent(\n            display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);\n      }\n      if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {\n        EGL14.eglDestroySurface(display, surface);\n      }\n      if (context != null) {\n        EGL14.eglDestroyContext(display, context);\n      }\n      // EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]).\n      if (Util.SDK_INT >= 19) {\n        EGL14.eglReleaseThread();\n      }\n      if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) {\n        // Android is unusual in that it uses a reference-counted EGLDisplay.  So for\n        // every eglInitialize() we need an eglTerminate().\n        EGL14.eglTerminate(display);\n      }\n      display = null;\n      context = null;\n      surface = null;\n      texture = null;\n    }\n  }\n\n  /**\n   * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}.\n   */\n  public SurfaceTexture getSurfaceTexture() {\n    return Assertions.checkNotNull(texture);\n  }\n\n  // SurfaceTexture.OnFrameAvailableListener\n\n  @Override\n  public void onFrameAvailable(SurfaceTexture surfaceTexture) {\n    handler.post(this);\n  }\n\n  // Runnable\n\n  @Override\n  public void run() {\n    // Run on the provided handler thread when a new image frame is available.\n    dispatchOnFrameAvailable();\n    if (texture != null) {\n      try {\n        texture.updateTexImage();\n      } catch (RuntimeException e) {\n        // Ignore\n      }\n    }\n  }\n\n  private void dispatchOnFrameAvailable() {\n    if (callback != null) {\n      callback.onFrameAvailable();\n    }\n  }\n\n  private static EGLDisplay getDefaultDisplay() {\n    EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);\n    if (display == null) {\n      throw new GlException(\"eglGetDisplay failed\");\n    }\n\n    int[] version = new int[2];\n    boolean eglInitialized =\n        EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);\n    if (!eglInitialized) {\n      throw new GlException(\"eglInitialize failed\");\n    }\n    return display;\n  }\n\n  private static EGLConfig chooseEGLConfig(EGLDisplay display) {\n    EGLConfig[] configs = new EGLConfig[1];\n    int[] numConfigs = new int[1];\n    boolean success =\n        EGL14.eglChooseConfig(\n            display,\n            EGL_CONFIG_ATTRIBUTES,\n            /* attrib_listOffset= */ 0,\n            configs,\n            /* configsOffset= */ 0,\n            /* config_size= */ 1,\n            numConfigs,\n            /* num_configOffset= */ 0);\n    if (!success || numConfigs[0] <= 0 || configs[0] == null) {\n      throw new GlException(\n          Util.formatInvariant(\n              /* format= */ \"eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s\",\n              success, numConfigs[0], configs[0]));\n    }\n\n    return configs[0];\n  }\n\n  private static EGLContext createEGLContext(\n      EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {\n    int[] glAttributes;\n    if (secureMode == SECURE_MODE_NONE) {\n      glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};\n    } else {\n      glAttributes =\n          new int[] {\n            EGL14.EGL_CONTEXT_CLIENT_VERSION,\n            2,\n            EGL_PROTECTED_CONTENT_EXT,\n            EGL14.EGL_TRUE,\n            EGL14.EGL_NONE\n          };\n    }\n    EGLContext context =\n        EGL14.eglCreateContext(\n            display, config, EGL14.EGL_NO_CONTEXT, glAttributes, 0);\n    if (context == null) {\n      throw new GlException(\"eglCreateContext failed\");\n    }\n    return context;\n  }\n\n  private static EGLSurface createEGLSurface(\n      EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {\n    EGLSurface surface;\n    if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {\n      surface = EGL14.EGL_NO_SURFACE;\n    } else {\n      int[] pbufferAttributes;\n      if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {\n        pbufferAttributes =\n            new int[] {\n              EGL14.EGL_WIDTH,\n              EGL_SURFACE_WIDTH,\n              EGL14.EGL_HEIGHT,\n              EGL_SURFACE_HEIGHT,\n              EGL_PROTECTED_CONTENT_EXT,\n              EGL14.EGL_TRUE,\n              EGL14.EGL_NONE\n            };\n      } else {\n        pbufferAttributes =\n            new int[] {\n              EGL14.EGL_WIDTH,\n              EGL_SURFACE_WIDTH,\n              EGL14.EGL_HEIGHT,\n              EGL_SURFACE_HEIGHT,\n              EGL14.EGL_NONE\n            };\n      }\n      surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);\n      if (surface == null) {\n        throw new GlException(\"eglCreatePbufferSurface failed\");\n      }\n    }\n\n    boolean eglMadeCurrent =\n        EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);\n    if (!eglMadeCurrent) {\n      throw new GlException(\"eglMakeCurrent failed\");\n    }\n    return surface;\n  }\n\n  private static void generateTextureIds(int[] textureIdHolder) {\n    GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);\n    GlUtil.checkGlError();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.util.Pair;\n\n/** Converts throwables into error codes and user readable error messages. */\npublic interface ErrorMessageProvider<T extends Throwable> {\n\n  /**\n   * Returns a pair consisting of an error code and a user readable error message for the given\n   * throwable.\n   *\n   * @param throwable The throwable for which an error code and message should be generated.\n   * @return A pair consisting of an error code and a user readable error message.\n   */\n  Pair<Integer, String> getErrorMessage(T throwable);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/EventDispatcher.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Handler;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * Event dispatcher which allows listener registration.\n *\n * @param <T> The type of listener.\n */\npublic final class EventDispatcher<T> {\n\n  /** Functional interface to send an event. */\n  public interface Event<T> {\n\n    /**\n     * Sends the event to a listener.\n     *\n     * @param listener The listener to send the event to.\n     */\n    void sendTo(T listener);\n  }\n\n  /** The list of listeners and handlers. */\n  private final CopyOnWriteArrayList<HandlerAndListener<T>> listeners;\n\n  /** Creates an event dispatcher. */\n  public EventDispatcher() {\n    listeners = new CopyOnWriteArrayList<>();\n  }\n\n  /** Adds a listener to the event dispatcher. */\n  public void addListener(Handler handler, T eventListener) {\n    Assertions.checkArgument(handler != null && eventListener != null);\n    removeListener(eventListener);\n    listeners.add(new HandlerAndListener<>(handler, eventListener));\n  }\n\n  /** Removes a listener from the event dispatcher. */\n  public void removeListener(T eventListener) {\n    for (HandlerAndListener<T> handlerAndListener : listeners) {\n      if (handlerAndListener.listener == eventListener) {\n        handlerAndListener.release();\n        listeners.remove(handlerAndListener);\n      }\n    }\n  }\n\n  /**\n   * Dispatches an event to all registered listeners.\n   *\n   * @param event The {@link Event}.\n   */\n  public void dispatch(Event<T> event) {\n    for (HandlerAndListener<T> handlerAndListener : listeners) {\n      handlerAndListener.dispatch(event);\n    }\n  }\n\n  private static final class HandlerAndListener<T> {\n\n    private final Handler handler;\n    private final T listener;\n\n    private boolean released;\n\n    public HandlerAndListener(Handler handler, T eventListener) {\n      this.handler = handler;\n      this.listener = eventListener;\n    }\n\n    public void release() {\n      released = true;\n    }\n\n    public void dispatch(Event<T> event) {\n      handler.post(\n          () -> {\n            if (!released) {\n              event.sendTo(listener);\n            }\n          });\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/EventLogger.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.SystemClock;\nimport android.view.Surface;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.PlaybackSuppressionReason;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport java.io.IOException;\nimport java.text.NumberFormat;\nimport java.util.Locale;\n\n/** Logs events from {@link Player} and other core components using {@link Log}. */\n@SuppressWarnings(\"UngroupedOverloads\")\npublic class EventLogger implements AnalyticsListener {\n\n  private static final String DEFAULT_TAG = \"EventLogger\";\n  private static final int MAX_TIMELINE_ITEM_LINES = 3;\n  private static final NumberFormat TIME_FORMAT;\n  static {\n    TIME_FORMAT = NumberFormat.getInstance(Locale.US);\n    TIME_FORMAT.setMinimumFractionDigits(2);\n    TIME_FORMAT.setMaximumFractionDigits(2);\n    TIME_FORMAT.setGroupingUsed(false);\n  }\n\n  @Nullable private final MappingTrackSelector trackSelector;\n  private final String tag;\n  private final Timeline.Window window;\n  private final Timeline.Period period;\n  private final long startTimeMs;\n\n  /**\n   * Creates event logger.\n   *\n   * @param trackSelector The mapping track selector used by the player. May be null if detailed\n   *     logging of track mapping is not required.\n   */\n  public EventLogger(@Nullable MappingTrackSelector trackSelector) {\n    this(trackSelector, DEFAULT_TAG);\n  }\n\n  /**\n   * Creates event logger.\n   *\n   * @param trackSelector The mapping track selector used by the player. May be null if detailed\n   *     logging of track mapping is not required.\n   * @param tag The tag used for logging.\n   */\n  public EventLogger(@Nullable MappingTrackSelector trackSelector, String tag) {\n    this.trackSelector = trackSelector;\n    this.tag = tag;\n    window = new Timeline.Window();\n    period = new Timeline.Period();\n    startTimeMs = SystemClock.elapsedRealtime();\n  }\n\n  // AnalyticsListener\n\n  @Override\n  public void onLoadingChanged(EventTime eventTime, boolean isLoading) {\n    logd(eventTime, \"loading\", Boolean.toString(isLoading));\n  }\n\n  @Override\n  public void onPlayerStateChanged(\n      EventTime eventTime, boolean playWhenReady, @Player.State int state) {\n    logd(eventTime, \"state\", playWhenReady + \", \" + getStateString(state));\n  }\n\n  @Override\n  public void onPlaybackSuppressionReasonChanged(\n      EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {\n    logd(\n        eventTime,\n        \"playbackSuppressionReason\",\n        getPlaybackSuppressionReasonString(playbackSuppressionReason));\n  }\n\n  @Override\n  public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {\n    logd(eventTime, \"isPlaying\", Boolean.toString(isPlaying));\n  }\n\n  @Override\n  public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {\n    logd(eventTime, \"repeatMode\", getRepeatModeString(repeatMode));\n  }\n\n  @Override\n  public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {\n    logd(eventTime, \"shuffleModeEnabled\", Boolean.toString(shuffleModeEnabled));\n  }\n\n  @Override\n  public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {\n    logd(eventTime, \"positionDiscontinuity\", getDiscontinuityReasonString(reason));\n  }\n\n  @Override\n  public void onSeekStarted(EventTime eventTime) {\n    logd(eventTime, \"seekStarted\");\n  }\n\n  @Override\n  public void onPlaybackParametersChanged(\n      EventTime eventTime, PlaybackParameters playbackParameters) {\n    logd(\n        eventTime,\n        \"playbackParameters\",\n        Util.formatInvariant(\n            \"speed=%.2f, pitch=%.2f, skipSilence=%s\",\n            playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence));\n  }\n\n  @Override\n  public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {\n    int periodCount = eventTime.timeline.getPeriodCount();\n    int windowCount = eventTime.timeline.getWindowCount();\n    logd(\n        \"timeline [\"\n            + getEventTimeString(eventTime)\n            + \", periodCount=\"\n            + periodCount\n            + \", windowCount=\"\n            + windowCount\n            + \", reason=\"\n            + getTimelineChangeReasonString(reason));\n    for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {\n      eventTime.timeline.getPeriod(i, period);\n      logd(\"  \" + \"period [\" + getTimeString(period.getDurationMs()) + \"]\");\n    }\n    if (periodCount > MAX_TIMELINE_ITEM_LINES) {\n      logd(\"  ...\");\n    }\n    for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {\n      eventTime.timeline.getWindow(i, window);\n      logd(\n          \"  \"\n              + \"window [\"\n              + getTimeString(window.getDurationMs())\n              + \", \"\n              + window.isSeekable\n              + \", \"\n              + window.isDynamic\n              + \"]\");\n    }\n    if (windowCount > MAX_TIMELINE_ITEM_LINES) {\n      logd(\"  ...\");\n    }\n    logd(\"]\");\n  }\n\n  @Override\n  public void onPlayerError(EventTime eventTime, ExoPlaybackException e) {\n    loge(eventTime, \"playerFailed\", e);\n  }\n\n  @Override\n  public void onTracksChanged(\n      EventTime eventTime, TrackGroupArray ignored, TrackSelectionArray trackSelections) {\n    MappedTrackInfo mappedTrackInfo =\n        trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null;\n    if (mappedTrackInfo == null) {\n      logd(eventTime, \"tracks\", \"[]\");\n      return;\n    }\n    logd(\"tracks [\" + getEventTimeString(eventTime) + \", \");\n    // Log tracks associated to renderers.\n    int rendererCount = mappedTrackInfo.getRendererCount();\n    for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {\n      TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);\n      TrackSelection trackSelection = trackSelections.get(rendererIndex);\n      if (rendererTrackGroups.length > 0) {\n        logd(\"  Renderer:\" + rendererIndex + \" [\");\n        for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {\n          TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);\n          String adaptiveSupport =\n              getAdaptiveSupportString(\n                  trackGroup.length,\n                  mappedTrackInfo.getAdaptiveSupport(\n                      rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false));\n          logd(\"    Group:\" + groupIndex + \", adaptive_supported=\" + adaptiveSupport + \" [\");\n          for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n            String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);\n            String formatSupport =\n                RendererCapabilities.getFormatSupportString(\n                    mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex));\n            logd(\n                \"      \"\n                    + status\n                    + \" Track:\"\n                    + trackIndex\n                    + \", \"\n                    + Format.toLogString(trackGroup.getFormat(trackIndex))\n                    + \", supported=\"\n                    + formatSupport);\n          }\n          logd(\"    ]\");\n        }\n        // Log metadata for at most one of the tracks selected for the renderer.\n        if (trackSelection != null) {\n          for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {\n            Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;\n            if (metadata != null) {\n              logd(\"    Metadata [\");\n              printMetadata(metadata, \"      \");\n              logd(\"    ]\");\n              break;\n            }\n          }\n        }\n        logd(\"  ]\");\n      }\n    }\n    // Log tracks not associated with a renderer.\n    TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups();\n    if (unassociatedTrackGroups.length > 0) {\n      logd(\"  Renderer:None [\");\n      for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {\n        logd(\"    Group:\" + groupIndex + \" [\");\n        TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);\n        for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n          String status = getTrackStatusString(false);\n          String formatSupport =\n              RendererCapabilities.getFormatSupportString(\n                  RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);\n          logd(\n              \"      \"\n                  + status\n                  + \" Track:\"\n                  + trackIndex\n                  + \", \"\n                  + Format.toLogString(trackGroup.getFormat(trackIndex))\n                  + \", supported=\"\n                  + formatSupport);\n        }\n        logd(\"    ]\");\n      }\n      logd(\"  ]\");\n    }\n    logd(\"]\");\n  }\n\n  @Override\n  public void onSeekProcessed(EventTime eventTime) {\n    logd(eventTime, \"seekProcessed\");\n  }\n\n  @Override\n  public void onMetadata(EventTime eventTime, Metadata metadata) {\n    logd(\"metadata [\" + getEventTimeString(eventTime) + \", \");\n    printMetadata(metadata, \"  \");\n    logd(\"]\");\n  }\n\n  @Override\n  public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) {\n    logd(eventTime, \"decoderEnabled\", Util.getTrackTypeString(trackType));\n  }\n\n  @Override\n  public void onAudioSessionId(EventTime eventTime, int audioSessionId) {\n    logd(eventTime, \"audioSessionId\", Integer.toString(audioSessionId));\n  }\n\n  @Override\n  public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {\n    logd(\n        eventTime,\n        \"audioAttributes\",\n        audioAttributes.contentType\n            + \",\"\n            + audioAttributes.flags\n            + \",\"\n            + audioAttributes.usage\n            + \",\"\n            + audioAttributes.allowedCapturePolicy);\n  }\n\n  @Override\n  public void onVolumeChanged(EventTime eventTime, float volume) {\n    logd(eventTime, \"volume\", Float.toString(volume));\n  }\n\n  @Override\n  public void onDecoderInitialized(\n      EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {\n    logd(eventTime, \"decoderInitialized\", Util.getTrackTypeString(trackType) + \", \" + decoderName);\n  }\n\n  @Override\n  public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {\n    logd(\n        eventTime,\n        \"decoderInputFormat\",\n        Util.getTrackTypeString(trackType) + \", \" + Format.toLogString(format));\n  }\n\n  @Override\n  public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) {\n    logd(eventTime, \"decoderDisabled\", Util.getTrackTypeString(trackType));\n  }\n\n  @Override\n  public void onAudioUnderrun(\n      EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n    loge(\n        eventTime,\n        \"audioTrackUnderrun\",\n        bufferSize + \", \" + bufferSizeMs + \", \" + elapsedSinceLastFeedMs + \"]\",\n        null);\n  }\n\n  @Override\n  public void onDroppedVideoFrames(EventTime eventTime, int count, long elapsedMs) {\n    logd(eventTime, \"droppedFrames\", Integer.toString(count));\n  }\n\n  @Override\n  public void onVideoSizeChanged(\n      EventTime eventTime,\n      int width,\n      int height,\n      int unappliedRotationDegrees,\n      float pixelWidthHeightRatio) {\n    logd(eventTime, \"videoSize\", width + \", \" + height);\n  }\n\n  @Override\n  public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {\n    logd(eventTime, \"renderedFirstFrame\", String.valueOf(surface));\n  }\n\n  @Override\n  public void onMediaPeriodCreated(EventTime eventTime) {\n    logd(eventTime, \"mediaPeriodCreated\");\n  }\n\n  @Override\n  public void onMediaPeriodReleased(EventTime eventTime) {\n    logd(eventTime, \"mediaPeriodReleased\");\n  }\n\n  @Override\n  public void onLoadStarted(\n      EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onLoadError(\n      EventTime eventTime,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData,\n      IOException error,\n      boolean wasCanceled) {\n    printInternalError(eventTime, \"loadError\", error);\n  }\n\n  @Override\n  public void onLoadCanceled(\n      EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onLoadCompleted(\n      EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onReadingStarted(EventTime eventTime) {\n    logd(eventTime, \"mediaPeriodReadingStarted\");\n  }\n\n  @Override\n  public void onBandwidthEstimate(\n      EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {\n    logd(eventTime, \"surfaceSize\", width + \", \" + height);\n  }\n\n  @Override\n  public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {\n    logd(eventTime, \"upstreamDiscarded\", Format.toLogString(mediaLoadData.trackFormat));\n  }\n\n  @Override\n  public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {\n    logd(eventTime, \"downstreamFormat\", Format.toLogString(mediaLoadData.trackFormat));\n  }\n\n  @Override\n  public void onDrmSessionAcquired(EventTime eventTime) {\n    logd(eventTime, \"drmSessionAcquired\");\n  }\n\n  @Override\n  public void onDrmSessionManagerError(EventTime eventTime, Exception e) {\n    printInternalError(eventTime, \"drmSessionManagerError\", e);\n  }\n\n  @Override\n  public void onDrmKeysRestored(EventTime eventTime) {\n    logd(eventTime, \"drmKeysRestored\");\n  }\n\n  @Override\n  public void onDrmKeysRemoved(EventTime eventTime) {\n    logd(eventTime, \"drmKeysRemoved\");\n  }\n\n  @Override\n  public void onDrmKeysLoaded(EventTime eventTime) {\n    logd(eventTime, \"drmKeysLoaded\");\n  }\n\n  @Override\n  public void onDrmSessionReleased(EventTime eventTime) {\n    logd(eventTime, \"drmSessionReleased\");\n  }\n\n  /**\n   * Logs a debug message.\n   *\n   * @param msg The message to log.\n   */\n  protected void logd(String msg) {\n    Log.d(tag, msg);\n  }\n\n  /**\n   * Logs an error message and exception.\n   *\n   * @param msg The message to log.\n   * @param tr The exception to log.\n   */\n  protected void loge(String msg, @Nullable Throwable tr) {\n    Log.e(tag, msg, tr);\n  }\n\n  // Internal methods\n\n  private void logd(EventTime eventTime, String eventName) {\n    logd(getEventString(eventTime, eventName));\n  }\n\n  private void logd(EventTime eventTime, String eventName, String eventDescription) {\n    logd(getEventString(eventTime, eventName, eventDescription));\n  }\n\n  private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) {\n    loge(getEventString(eventTime, eventName), throwable);\n  }\n\n  private void loge(\n      EventTime eventTime,\n      String eventName,\n      String eventDescription,\n      @Nullable Throwable throwable) {\n    loge(getEventString(eventTime, eventName, eventDescription), throwable);\n  }\n\n  private void printInternalError(EventTime eventTime, String type, Exception e) {\n    loge(eventTime, \"internalError\", type, e);\n  }\n\n  private void printMetadata(Metadata metadata, String prefix) {\n    for (int i = 0; i < metadata.length(); i++) {\n      logd(prefix + metadata.get(i));\n    }\n  }\n\n  private String getEventString(EventTime eventTime, String eventName) {\n    return eventName + \" [\" + getEventTimeString(eventTime) + \"]\";\n  }\n\n  private String getEventString(EventTime eventTime, String eventName, String eventDescription) {\n    return eventName + \" [\" + getEventTimeString(eventTime) + \", \" + eventDescription + \"]\";\n  }\n\n  private String getEventTimeString(EventTime eventTime) {\n    String windowPeriodString = \"window=\" + eventTime.windowIndex;\n    if (eventTime.mediaPeriodId != null) {\n      windowPeriodString +=\n          \", period=\" + eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);\n      if (eventTime.mediaPeriodId.isAd()) {\n        windowPeriodString += \", adGroup=\" + eventTime.mediaPeriodId.adGroupIndex;\n        windowPeriodString += \", ad=\" + eventTime.mediaPeriodId.adIndexInAdGroup;\n      }\n    }\n    return \"eventTime=\"\n        + getTimeString(eventTime.realtimeMs - startTimeMs)\n        + \", mediaPos=\"\n        + getTimeString(eventTime.currentPlaybackPositionMs)\n        + \", \"\n        + windowPeriodString;\n  }\n\n  private static String getTimeString(long timeMs) {\n    return timeMs == C.TIME_UNSET ? \"?\" : TIME_FORMAT.format((timeMs) / 1000f);\n  }\n\n  private static String getStateString(int state) {\n    switch (state) {\n      case Player.STATE_BUFFERING:\n        return \"BUFFERING\";\n      case Player.STATE_ENDED:\n        return \"ENDED\";\n      case Player.STATE_IDLE:\n        return \"IDLE\";\n      case Player.STATE_READY:\n        return \"READY\";\n      default:\n        return \"?\";\n    }\n  }\n\n  private static String getAdaptiveSupportString(\n      int trackCount, @AdaptiveSupport int adaptiveSupport) {\n    if (trackCount < 2) {\n      return \"N/A\";\n    }\n    switch (adaptiveSupport) {\n      case RendererCapabilities.ADAPTIVE_SEAMLESS:\n        return \"YES\";\n      case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:\n        return \"YES_NOT_SEAMLESS\";\n      case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:\n        return \"NO\";\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  // Suppressing reference equality warning because the track group stored in the track selection\n  // must point to the exact track group object to be considered part of it.\n  @SuppressWarnings(\"ReferenceEquality\")\n  private static String getTrackStatusString(\n      @Nullable TrackSelection selection, TrackGroup group, int trackIndex) {\n    return getTrackStatusString(selection != null && selection.getTrackGroup() == group\n        && selection.indexOf(trackIndex) != C.INDEX_UNSET);\n  }\n\n  private static String getTrackStatusString(boolean enabled) {\n    return enabled ? \"[X]\" : \"[ ]\";\n  }\n\n  private static String getRepeatModeString(@Player.RepeatMode int repeatMode) {\n    switch (repeatMode) {\n      case Player.REPEAT_MODE_OFF:\n        return \"OFF\";\n      case Player.REPEAT_MODE_ONE:\n        return \"ONE\";\n      case Player.REPEAT_MODE_ALL:\n        return \"ALL\";\n      default:\n        return \"?\";\n    }\n  }\n\n  private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) {\n    switch (reason) {\n      case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION:\n        return \"PERIOD_TRANSITION\";\n      case Player.DISCONTINUITY_REASON_SEEK:\n        return \"SEEK\";\n      case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:\n        return \"SEEK_ADJUSTMENT\";\n      case Player.DISCONTINUITY_REASON_AD_INSERTION:\n        return \"AD_INSERTION\";\n      case Player.DISCONTINUITY_REASON_INTERNAL:\n        return \"INTERNAL\";\n      default:\n        return \"?\";\n    }\n  }\n\n  private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {\n    switch (reason) {\n      case Player.TIMELINE_CHANGE_REASON_PREPARED:\n        return \"PREPARED\";\n      case Player.TIMELINE_CHANGE_REASON_RESET:\n        return \"RESET\";\n      case Player.TIMELINE_CHANGE_REASON_DYNAMIC:\n        return \"DYNAMIC\";\n      default:\n        return \"?\";\n    }\n  }\n\n  private static String getPlaybackSuppressionReasonString(\n      @PlaybackSuppressionReason int playbackSuppressionReason) {\n    switch (playbackSuppressionReason) {\n      case Player.PLAYBACK_SUPPRESSION_REASON_NONE:\n        return \"NONE\";\n      case Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS:\n        return \"TRANSIENT_AUDIO_FOCUS_LOSS\";\n      default:\n        return \"?\";\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.flac.PictureFrame;\nimport com.google.android.exoplayer2.metadata.flac.VorbisComment;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** Holder for FLAC metadata. */\npublic final class FlacStreamMetadata {\n\n  private static final String TAG = \"FlacStreamMetadata\";\n\n  public final int minBlockSize;\n  public final int maxBlockSize;\n  public final int minFrameSize;\n  public final int maxFrameSize;\n  public final int sampleRate;\n  public final int channels;\n  public final int bitsPerSample;\n  public final long totalSamples;\n  @Nullable public final Metadata metadata;\n\n  private static final String SEPARATOR = \"=\";\n\n  /**\n   * Parses binary FLAC stream info metadata.\n   *\n   * @param data An array containing binary FLAC stream info metadata.\n   * @param offset The offset of the stream info metadata in {@code data}.\n   * @see <a href=\"https://xiph.org/flac/format.html#metadata_block_streaminfo\">FLAC format\n   *     METADATA_BLOCK_STREAMINFO</a>\n   */\n  public FlacStreamMetadata(byte[] data, int offset) {\n    ParsableBitArray scratch = new ParsableBitArray(data);\n    scratch.setPosition(offset * 8);\n    this.minBlockSize = scratch.readBits(16);\n    this.maxBlockSize = scratch.readBits(16);\n    this.minFrameSize = scratch.readBits(24);\n    this.maxFrameSize = scratch.readBits(24);\n    this.sampleRate = scratch.readBits(20);\n    this.channels = scratch.readBits(3) + 1;\n    this.bitsPerSample = scratch.readBits(5) + 1;\n    this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);\n    this.metadata = null;\n  }\n\n  /**\n   * @param minBlockSize Minimum block size of the FLAC stream.\n   * @param maxBlockSize Maximum block size of the FLAC stream.\n   * @param minFrameSize Minimum frame size of the FLAC stream.\n   * @param maxFrameSize Maximum frame size of the FLAC stream.\n   * @param sampleRate Sample rate of the FLAC stream.\n   * @param channels Number of channels of the FLAC stream.\n   * @param bitsPerSample Number of bits per sample of the FLAC stream.\n   * @param totalSamples Total samples of the FLAC stream.\n   * @param vorbisComments Vorbis comments. Each entry must be in key=value form.\n   * @param pictureFrames Picture frames.\n   * @see <a href=\"https://xiph.org/flac/format.html#metadata_block_streaminfo\">FLAC format\n   *     METADATA_BLOCK_STREAMINFO</a>\n   * @see <a href=\"https://xiph.org/flac/format.html#metadata_block_vorbis_comment\">FLAC format\n   *     METADATA_BLOCK_VORBIS_COMMENT</a>\n   * @see <a href=\"https://xiph.org/flac/format.html#metadata_block_picture\">FLAC format\n   *     METADATA_BLOCK_PICTURE</a>\n   */\n  public FlacStreamMetadata(\n      int minBlockSize,\n      int maxBlockSize,\n      int minFrameSize,\n      int maxFrameSize,\n      int sampleRate,\n      int channels,\n      int bitsPerSample,\n      long totalSamples,\n      List<String> vorbisComments,\n      List<PictureFrame> pictureFrames) {\n    this.minBlockSize = minBlockSize;\n    this.maxBlockSize = maxBlockSize;\n    this.minFrameSize = minFrameSize;\n    this.maxFrameSize = maxFrameSize;\n    this.sampleRate = sampleRate;\n    this.channels = channels;\n    this.bitsPerSample = bitsPerSample;\n    this.totalSamples = totalSamples;\n    this.metadata = buildMetadata(vorbisComments, pictureFrames);\n  }\n\n  /** Returns the maximum size for a decoded frame from the FLAC stream. */\n  public int maxDecodedFrameSize() {\n    return maxBlockSize * channels * (bitsPerSample / 8);\n  }\n\n  /** Returns the bit-rate of the FLAC stream. */\n  public int bitRate() {\n    return bitsPerSample * sampleRate * channels;\n  }\n\n  /** Returns the duration of the FLAC stream in microseconds. */\n  public long durationUs() {\n    return (totalSamples * 1000000L) / sampleRate;\n  }\n\n  /**\n   * Returns the sample index for the sample at given position.\n   *\n   * @param timeUs Time position in microseconds in the FLAC stream.\n   * @return The sample index for the sample at given position.\n   */\n  public long getSampleIndex(long timeUs) {\n    long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;\n    return Util.constrainValue(sampleIndex, 0, totalSamples - 1);\n  }\n\n  /** Returns the approximate number of bytes per frame for the current FLAC stream. */\n  public long getApproxBytesPerFrame() {\n    long approxBytesPerFrame;\n    if (maxFrameSize > 0) {\n      approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;\n    } else {\n      // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the\n      // default value for FLAC block-size, which is 4096.\n      long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;\n      approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;\n    }\n    return approxBytesPerFrame;\n  }\n\n  @Nullable\n  private static Metadata buildMetadata(\n      List<String> vorbisComments, List<PictureFrame> pictureFrames) {\n    if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {\n      return null;\n    }\n\n    ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();\n    for (int i = 0; i < vorbisComments.size(); i++) {\n      String vorbisComment = vorbisComments.get(i);\n      String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);\n      if (keyAndValue.length != 2) {\n        Log.w(TAG, \"Failed to parse vorbis comment: \" + vorbisComment);\n      } else {\n        VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);\n        metadataEntries.add(entry);\n      }\n    }\n    metadataEntries.addAll(pictureFrames);\n\n    return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/GlUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport static android.opengl.GLU.gluErrorString;\n\nimport android.opengl.GLES11Ext;\nimport android.opengl.GLES20;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\nimport java.nio.IntBuffer;\n\n/** GL utility methods. */\npublic final class GlUtil {\n  private static final String TAG = \"GlUtil\";\n\n  /** Class only contains static methods. */\n  private GlUtil() {}\n\n  /**\n   * If there is an OpenGl error, logs the error and if {@link\n   * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}.\n   */\n  public static void checkGlError() {\n    int lastError = GLES20.GL_NO_ERROR;\n    int error;\n    while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {\n      Log.e(TAG, \"glError \" + gluErrorString(error));\n      lastError = error;\n    }\n    if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) {\n      throw new RuntimeException(\"glError \" + gluErrorString(lastError));\n    }\n  }\n\n  /**\n   * Builds a GL shader program from vertex and fragment shader code.\n   *\n   * @param vertexCode GLES20 vertex shader program as arrays of strings. Strings are joined by\n   *     adding a new line character in between each of them.\n   * @param fragmentCode GLES20 fragment shader program as arrays of strings. Strings are joined by\n   *     adding a new line character in between each of them.\n   * @return GLES20 program id.\n   */\n  public static int compileProgram(String[] vertexCode, String[] fragmentCode) {\n    return compileProgram(TextUtils.join(\"\\n\", vertexCode), TextUtils.join(\"\\n\", fragmentCode));\n  }\n\n  /**\n   * Builds a GL shader program from vertex and fragment shader code.\n   *\n   * @param vertexCode GLES20 vertex shader program.\n   * @param fragmentCode GLES20 fragment shader program.\n   * @return GLES20 program id.\n   */\n  public static int compileProgram(String vertexCode, String fragmentCode) {\n    int program = GLES20.glCreateProgram();\n    checkGlError();\n\n    // Add the vertex and fragment shaders.\n    addShader(GLES20.GL_VERTEX_SHADER, vertexCode, program);\n    addShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode, program);\n\n    // Link and check for errors.\n    GLES20.glLinkProgram(program);\n    int[] linkStatus = new int[] {GLES20.GL_FALSE};\n    GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);\n    if (linkStatus[0] != GLES20.GL_TRUE) {\n      throwGlError(\"Unable to link shader program: \\n\" + GLES20.glGetProgramInfoLog(program));\n    }\n    checkGlError();\n\n    return program;\n  }\n\n  /**\n   * Allocates a FloatBuffer with the given data.\n   *\n   * @param data Used to initialize the new buffer.\n   */\n  public static FloatBuffer createBuffer(float[] data) {\n    return (FloatBuffer) createBuffer(data.length).put(data).flip();\n  }\n\n  /**\n   * Allocates a FloatBuffer.\n   *\n   * @param capacity The new buffer's capacity, in floats.\n   */\n  public static FloatBuffer createBuffer(int capacity) {\n    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity * C.BYTES_PER_FLOAT);\n    return byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();\n  }\n\n  /**\n   * Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and\n   * GL_CLAMP_TO_EDGE wrapping.\n   */\n  public static int createExternalTexture() {\n    int[] texId = new int[1];\n    GLES20.glGenTextures(1, IntBuffer.wrap(texId));\n    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);\n    GLES20.glTexParameteri(\n        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n    GLES20.glTexParameteri(\n        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n    GLES20.glTexParameteri(\n        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);\n    GLES20.glTexParameteri(\n        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);\n    checkGlError();\n    return texId[0];\n  }\n\n  private static void addShader(int type, String source, int program) {\n    int shader = GLES20.glCreateShader(type);\n    GLES20.glShaderSource(shader, source);\n    GLES20.glCompileShader(shader);\n\n    int[] result = new int[] {GLES20.GL_FALSE};\n    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);\n    if (result[0] != GLES20.GL_TRUE) {\n      throwGlError(GLES20.glGetShaderInfoLog(shader) + \", source: \" + source);\n    }\n\n    GLES20.glAttachShader(program, shader);\n    GLES20.glDeleteShader(shader);\n    checkGlError();\n  }\n\n  private static void throwGlError(String errorMsg) {\n    Log.e(TAG, errorMsg);\n    if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) {\n      throw new RuntimeException(errorMsg);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.Nullable;\n\n/**\n * An interface to call through to a {@link Handler}. Instances must be created by calling {@link\n * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases.\n */\npublic interface HandlerWrapper {\n\n  /** @see Handler#getLooper() */\n  Looper getLooper();\n\n  /** @see Handler#obtainMessage(int) */\n  Message obtainMessage(int what);\n\n  /** @see Handler#obtainMessage(int, Object) */\n  Message obtainMessage(int what, @Nullable Object obj);\n\n  /** @see Handler#obtainMessage(int, int, int) */\n  Message obtainMessage(int what, int arg1, int arg2);\n\n  /** @see Handler#obtainMessage(int, int, int, Object) */\n  Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj);\n\n  /** @see Handler#sendEmptyMessage(int) */\n  boolean sendEmptyMessage(int what);\n\n  /** @see Handler#sendEmptyMessageAtTime(int, long) */\n  boolean sendEmptyMessageAtTime(int what, long uptimeMs);\n\n  /** @see Handler#removeMessages(int) */\n  void removeMessages(int what);\n\n  /** @see Handler#removeCallbacksAndMessages(Object) */\n  void removeCallbacksAndMessages(@Nullable Object token);\n\n  /** @see Handler#post(Runnable) */\n  boolean post(Runnable runnable);\n\n  /** @see Handler#postDelayed(Runnable, long) */\n  boolean postDelayed(Runnable runnable, long delayMs);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.util.Arrays;\n\n/**\n * Configurable loader for native libraries.\n */\npublic final class LibraryLoader {\n\n  private static final String TAG = \"LibraryLoader\";\n\n  private String[] nativeLibraries;\n  private boolean loadAttempted;\n  private boolean isAvailable;\n\n  /**\n   * @param libraries The names of the libraries to load.\n   */\n  public LibraryLoader(String... libraries) {\n    nativeLibraries = libraries;\n  }\n\n  /**\n   * Overrides the names of the libraries to load. Must be called before any call to\n   * {@link #isAvailable()}.\n   */\n  public synchronized void setLibraries(String... libraries) {\n    Assertions.checkState(!loadAttempted, \"Cannot set libraries after loading\");\n    nativeLibraries = libraries;\n  }\n\n  /**\n   * Returns whether the underlying libraries are available, loading them if necessary.\n   */\n  public synchronized boolean isAvailable() {\n    if (loadAttempted) {\n      return isAvailable;\n    }\n    loadAttempted = true;\n    try {\n      for (String lib : nativeLibraries) {\n        System.loadLibrary(lib);\n      }\n      isAvailable = true;\n    } catch (UnsatisfiedLinkError exception) {\n      // Log a warning as an attempt to check for the library indicates that the app depends on an\n      // extension and generally would expect its native libraries to be available.\n      Log.w(TAG, \"Failed to load \" + Arrays.toString(nativeLibraries));\n    }\n    return isAvailable;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/Log.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.text.TextUtils;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Wrapper around {@link android.util.Log} which allows to set the log level. */\npublic final class Log {\n\n  /**\n   * Log level for ExoPlayer logcat logging. One of {@link #LOG_LEVEL_ALL}, {@link #LOG_LEVEL_INFO},\n   * {@link #LOG_LEVEL_WARNING}, {@link #LOG_LEVEL_ERROR} or {@link #LOG_LEVEL_OFF}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({LOG_LEVEL_ALL, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_OFF})\n  @interface LogLevel {}\n  /** Log level to log all messages. */\n  public static final int LOG_LEVEL_ALL = 0;\n  /** Log level to only log informative, warning and error messages. */\n  public static final int LOG_LEVEL_INFO = 1;\n  /** Log level to only log warning and error messages. */\n  public static final int LOG_LEVEL_WARNING = 2;\n  /** Log level to only log error messages. */\n  public static final int LOG_LEVEL_ERROR = 3;\n  /** Log level to disable all logging. */\n  public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE;\n\n  private static int logLevel = LOG_LEVEL_ALL;\n  private static boolean logStackTraces = true;\n\n  private Log() {}\n\n  /** Returns current {@link LogLevel} for ExoPlayer logcat logging. */\n  public static @LogLevel int getLogLevel() {\n    return logLevel;\n  }\n\n  /** Returns whether stack traces of {@link Throwable}s will be logged to logcat. */\n  public boolean getLogStackTraces() {\n    return logStackTraces;\n  }\n\n  /**\n   * Sets the {@link LogLevel} for ExoPlayer logcat logging.\n   *\n   * @param logLevel The new {@link LogLevel}.\n   */\n  public static void setLogLevel(@LogLevel int logLevel) {\n    Log.logLevel = logLevel;\n  }\n\n  /**\n   * Sets whether stack traces of {@link Throwable}s will be logged to logcat.\n   *\n   * @param logStackTraces Whether stack traces will be logged.\n   */\n  public static void setLogStackTraces(boolean logStackTraces) {\n    Log.logStackTraces = logStackTraces;\n  }\n\n  /** @see android.util.Log#d(String, String) */\n  public static void d(String tag, String message) {\n    if (logLevel == LOG_LEVEL_ALL) {\n      android.util.Log.d(tag, message);\n    }\n  }\n\n  /** @see android.util.Log#d(String, String, Throwable) */\n  public static void d(String tag, String message, @Nullable Throwable throwable) {\n    if (!logStackTraces) {\n      d(tag, appendThrowableMessage(message, throwable));\n    } else if (logLevel == LOG_LEVEL_ALL) {\n      android.util.Log.d(tag, message, throwable);\n    }\n  }\n\n  /** @see android.util.Log#i(String, String) */\n  public static void i(String tag, String message) {\n    if (logLevel <= LOG_LEVEL_INFO) {\n      android.util.Log.i(tag, message);\n    }\n  }\n\n  /** @see android.util.Log#i(String, String, Throwable) */\n  public static void i(String tag, String message, @Nullable Throwable throwable) {\n    if (!logStackTraces) {\n      i(tag, appendThrowableMessage(message, throwable));\n    } else if (logLevel <= LOG_LEVEL_INFO) {\n      android.util.Log.i(tag, message, throwable);\n    }\n  }\n\n  /** @see android.util.Log#w(String, String) */\n  public static void w(String tag, String message) {\n    if (logLevel <= LOG_LEVEL_WARNING) {\n      android.util.Log.w(tag, message);\n    }\n  }\n\n  /** @see android.util.Log#w(String, String, Throwable) */\n  public static void w(String tag, String message, @Nullable Throwable throwable) {\n    if (!logStackTraces) {\n      w(tag, appendThrowableMessage(message, throwable));\n    } else if (logLevel <= LOG_LEVEL_WARNING) {\n      android.util.Log.w(tag, message, throwable);\n    }\n  }\n\n  /** @see android.util.Log#e(String, String) */\n  public static void e(String tag, String message) {\n    if (logLevel <= LOG_LEVEL_ERROR) {\n      android.util.Log.e(tag, message);\n    }\n  }\n\n  /** @see android.util.Log#e(String, String, Throwable) */\n  public static void e(String tag, String message, @Nullable Throwable throwable) {\n    if (!logStackTraces) {\n      e(tag, appendThrowableMessage(message, throwable));\n    } else if (logLevel <= LOG_LEVEL_ERROR) {\n      android.util.Log.e(tag, message, throwable);\n    }\n  }\n\n  private static String appendThrowableMessage(String message, @Nullable Throwable throwable) {\n    if (throwable == null) {\n      return message;\n    }\n    String throwableMessage = throwable.getMessage();\n    return TextUtils.isEmpty(throwableMessage) ? message : message + \" - \" + throwableMessage;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/LongArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.util.Arrays;\n\n/**\n * An append-only, auto-growing {@code long[]}.\n */\npublic final class LongArray {\n\n  private static final int DEFAULT_INITIAL_CAPACITY = 32;\n\n  private int size;\n  private long[] values;\n\n  public LongArray() {\n    this(DEFAULT_INITIAL_CAPACITY);\n  }\n\n  /**\n   * @param initialCapacity The initial capacity of the array.\n   */\n  public LongArray(int initialCapacity) {\n    values = new long[initialCapacity];\n  }\n\n  /**\n   * Appends a value.\n   *\n   * @param value The value to append.\n   */\n  public void add(long value) {\n    if (size == values.length) {\n      values = Arrays.copyOf(values, size * 2);\n    }\n    values[size++] = value;\n  }\n\n  /**\n   * Returns the value at a specified index.\n   *\n   * @param index The index.\n   * @return The corresponding value.\n   * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to\n   *     {@link #size()}.\n   */\n  public long get(int index) {\n    if (index < 0 || index >= size) {\n      throw new IndexOutOfBoundsException(\"Invalid index \" + index + \", size is \" + size);\n    }\n    return values[index];\n  }\n\n  /**\n   * Returns the current size of the array.\n   */\n  public int size() {\n    return size;\n  }\n\n  /**\n   * Copies the current values into a newly allocated primitive array.\n   *\n   * @return The primitive array containing the copied values.\n   */\n  public long[] toArray() {\n    return Arrays.copyOf(values, size);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/MediaClock.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport com.google.android.exoplayer2.PlaybackParameters;\n\n/**\n * Tracks the progression of media time.\n */\npublic interface MediaClock {\n\n  /**\n   * Returns the current media position in microseconds.\n   */\n  long getPositionUs();\n\n  /**\n   * Attempts to set the playback parameters. The media clock may override these parameters if they\n   * are not supported.\n   *\n   * @param playbackParameters The playback parameters to attempt to set.\n   */\n  void setPlaybackParameters(PlaybackParameters playbackParameters);\n\n  /**\n   * Returns the active playback parameters.\n   */\n  PlaybackParameters getPlaybackParameters();\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.util.ArrayList;\n\n/**\n * Defines common MIME types and helper methods.\n */\npublic final class MimeTypes {\n\n  public static final String BASE_TYPE_VIDEO = \"video\";\n  public static final String BASE_TYPE_AUDIO = \"audio\";\n  public static final String BASE_TYPE_TEXT = \"text\";\n  public static final String BASE_TYPE_APPLICATION = \"application\";\n\n  public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + \"/mp4\";\n  public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + \"/webm\";\n  public static final String VIDEO_H263 = BASE_TYPE_VIDEO + \"/3gpp\";\n  public static final String VIDEO_H264 = BASE_TYPE_VIDEO + \"/avc\";\n  public static final String VIDEO_H265 = BASE_TYPE_VIDEO + \"/hevc\";\n  public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + \"/x-vnd.on2.vp8\";\n  public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + \"/x-vnd.on2.vp9\";\n  public static final String VIDEO_AV1 = BASE_TYPE_VIDEO + \"/av01\";\n  public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + \"/mp4v-es\";\n  public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + \"/mpeg\";\n  public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + \"/mpeg2\";\n  public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + \"/wvc1\";\n  public static final String VIDEO_DIVX = BASE_TYPE_VIDEO + \"/divx\";\n  public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + \"/dolby-vision\";\n  public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + \"/x-unknown\";\n\n  public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + \"/mp4\";\n  public static final String AUDIO_AAC = BASE_TYPE_AUDIO + \"/mp4a-latm\";\n  public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + \"/webm\";\n  public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + \"/mpeg\";\n  public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + \"/mpeg-L1\";\n  public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + \"/mpeg-L2\";\n  public static final String AUDIO_RAW = BASE_TYPE_AUDIO + \"/raw\";\n  public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + \"/g711-alaw\";\n  public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + \"/g711-mlaw\";\n  public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + \"/ac3\";\n  public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + \"/eac3\";\n  public static final String AUDIO_E_AC3_JOC = BASE_TYPE_AUDIO + \"/eac3-joc\";\n  public static final String AUDIO_AC4 = BASE_TYPE_AUDIO + \"/ac4\";\n  public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + \"/true-hd\";\n  public static final String AUDIO_DTS = BASE_TYPE_AUDIO + \"/vnd.dts\";\n  public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + \"/vnd.dts.hd\";\n  public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + \"/vnd.dts.hd;profile=lbr\";\n  public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + \"/vorbis\";\n  public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + \"/opus\";\n  public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + \"/3gpp\";\n  public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + \"/amr-wb\";\n  public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + \"/flac\";\n  public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + \"/alac\";\n  public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + \"/gsm\";\n  public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + \"/x-unknown\";\n\n  public static final String TEXT_VTT = BASE_TYPE_TEXT + \"/vtt\";\n  public static final String TEXT_SSA = BASE_TYPE_TEXT + \"/x-ssa\";\n\n  public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + \"/mp4\";\n  public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + \"/webm\";\n  public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + \"/dash+xml\";\n  public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + \"/x-mpegURL\";\n  public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + \"/vnd.ms-sstr+xml\";\n  public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + \"/id3\";\n  public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + \"/cea-608\";\n  public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + \"/cea-708\";\n  public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + \"/x-subrip\";\n  public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + \"/ttml+xml\";\n  public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + \"/x-quicktime-tx3g\";\n  public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + \"/x-mp4-vtt\";\n  public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + \"/x-mp4-cea-608\";\n  public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + \"/x-rawcc\";\n  public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + \"/vobsub\";\n  public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + \"/pgs\";\n  public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + \"/x-scte35\";\n  public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + \"/x-camera-motion\";\n  public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + \"/x-emsg\";\n  public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + \"/dvbsubs\";\n  public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + \"/x-exif\";\n  public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + \"/x-icy\";\n\n  private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();\n\n  /**\n   * Registers a custom MIME type. Most applications do not need to call this method, as handling of\n   * standard MIME types is built in. These built-in MIME types take precedence over any registered\n   * via this method. If this method is used, it must be called before creating any player(s).\n   *\n   * @param mimeType The custom MIME type to register.\n   * @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type.\n   * @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type.\n   *     This value is ignored if the top-level type of {@code mimeType} is audio, video or text.\n   */\n  public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) {\n    CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType);\n    int customMimeTypeCount = customMimeTypes.size();\n    for (int i = 0; i < customMimeTypeCount; i++) {\n      if (mimeType.equals(customMimeTypes.get(i).mimeType)) {\n        customMimeTypes.remove(i);\n        break;\n      }\n    }\n    customMimeTypes.add(customMimeType);\n  }\n\n  /** Returns whether the given string is an audio mime type. */\n  public static boolean isAudio(@Nullable String mimeType) {\n    return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));\n  }\n\n  /** Returns whether the given string is a video mime type. */\n  public static boolean isVideo(@Nullable String mimeType) {\n    return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));\n  }\n\n  /** Returns whether the given string is a text mime type. */\n  public static boolean isText(@Nullable String mimeType) {\n    return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));\n  }\n\n  /** Returns whether the given string is an application mime type. */\n  public static boolean isApplication(@Nullable String mimeType) {\n    return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));\n  }\n\n  /**\n   * Derives a video sample mimeType from a codecs attribute.\n   *\n   * @param codecs The codecs attribute.\n   * @return The derived video mimeType, or null if it could not be derived.\n   */\n  public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) {\n    if (codecs == null) {\n      return null;\n    }\n    String[] codecList = Util.splitCodecs(codecs);\n    for (String codec : codecList) {\n      String mimeType = getMediaMimeType(codec);\n      if (mimeType != null && isVideo(mimeType)) {\n        return mimeType;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Derives a audio sample mimeType from a codecs attribute.\n   *\n   * @param codecs The codecs attribute.\n   * @return The derived audio mimeType, or null if it could not be derived.\n   */\n  public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) {\n    if (codecs == null) {\n      return null;\n    }\n    String[] codecList = Util.splitCodecs(codecs);\n    for (String codec : codecList) {\n      String mimeType = getMediaMimeType(codec);\n      if (mimeType != null && isAudio(mimeType)) {\n        return mimeType;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Derives a mimeType from a codec identifier, as defined in RFC 6381.\n   *\n   * @param codec The codec identifier to derive.\n   * @return The mimeType, or null if it could not be derived.\n   */\n  public static @Nullable String getMediaMimeType(@Nullable String codec) {\n    if (codec == null) {\n      return null;\n    }\n    codec = Util.toLowerInvariant(codec.trim());\n    if (codec.startsWith(\"avc1\") || codec.startsWith(\"avc3\")) {\n      return MimeTypes.VIDEO_H264;\n    } else if (codec.startsWith(\"hev1\") || codec.startsWith(\"hvc1\")) {\n      return MimeTypes.VIDEO_H265;\n    } else if (codec.startsWith(\"dvav\")\n        || codec.startsWith(\"dva1\")\n        || codec.startsWith(\"dvhe\")\n        || codec.startsWith(\"dvh1\")) {\n      return MimeTypes.VIDEO_DOLBY_VISION;\n    } else if (codec.startsWith(\"av01\")) {\n      return MimeTypes.VIDEO_AV1;\n    } else if (codec.startsWith(\"vp9\") || codec.startsWith(\"vp09\")) {\n      return MimeTypes.VIDEO_VP9;\n    } else if (codec.startsWith(\"vp8\") || codec.startsWith(\"vp08\")) {\n      return MimeTypes.VIDEO_VP8;\n    } else if (codec.startsWith(\"mp4a\")) {\n      String mimeType = null;\n      if (codec.startsWith(\"mp4a.\")) {\n        String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix\n        if (objectTypeString.length() >= 2) {\n          try {\n            String objectTypeHexString = Util.toUpperInvariant(objectTypeString.substring(0, 2));\n            int objectTypeInt = Integer.parseInt(objectTypeHexString, 16);\n            mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt);\n          } catch (NumberFormatException ignored) {\n            // ignored\n          }\n        }\n      }\n      return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType;\n    } else if (codec.startsWith(\"ac-3\") || codec.startsWith(\"dac3\")) {\n      return MimeTypes.AUDIO_AC3;\n    } else if (codec.startsWith(\"ec-3\") || codec.startsWith(\"dec3\")) {\n      return MimeTypes.AUDIO_E_AC3;\n    } else if (codec.startsWith(\"ec+3\")) {\n      return MimeTypes.AUDIO_E_AC3_JOC;\n    } else if (codec.startsWith(\"ac-4\") || codec.startsWith(\"dac4\")) {\n      return MimeTypes.AUDIO_AC4;\n    } else if (codec.startsWith(\"dtsc\") || codec.startsWith(\"dtse\")) {\n      return MimeTypes.AUDIO_DTS;\n    } else if (codec.startsWith(\"dtsh\") || codec.startsWith(\"dtsl\")) {\n      return MimeTypes.AUDIO_DTS_HD;\n    } else if (codec.startsWith(\"opus\")) {\n      return MimeTypes.AUDIO_OPUS;\n    } else if (codec.startsWith(\"vorbis\")) {\n      return MimeTypes.AUDIO_VORBIS;\n    } else if (codec.startsWith(\"flac\")) {\n      return MimeTypes.AUDIO_FLAC;\n    } else {\n      return getCustomMimeTypeForCodec(codec);\n    }\n  }\n\n  /**\n   * Derives a mimeType from MP4 object type identifier, as defined in RFC 6381 and\n   * https://mp4ra.org/#/object_types.\n   *\n   * @param objectType The objectType identifier to derive.\n   * @return The mimeType, or null if it could not be derived.\n   */\n  @Nullable\n  public static String getMimeTypeFromMp4ObjectType(int objectType) {\n    switch (objectType) {\n      case 0x20:\n        return MimeTypes.VIDEO_MP4V;\n      case 0x21:\n        return MimeTypes.VIDEO_H264;\n      case 0x23:\n        return MimeTypes.VIDEO_H265;\n      case 0x60:\n      case 0x61:\n      case 0x62:\n      case 0x63:\n      case 0x64:\n      case 0x65:\n        return MimeTypes.VIDEO_MPEG2;\n      case 0x6A:\n        return MimeTypes.VIDEO_MPEG;\n      case 0x69:\n      case 0x6B:\n        return MimeTypes.AUDIO_MPEG;\n      case 0xA3:\n        return MimeTypes.VIDEO_VC1;\n      case 0xB1:\n        return MimeTypes.VIDEO_VP9;\n      case 0x40:\n      case 0x66:\n      case 0x67:\n      case 0x68:\n        return MimeTypes.AUDIO_AAC;\n      case 0xA5:\n        return MimeTypes.AUDIO_AC3;\n      case 0xA6:\n        return MimeTypes.AUDIO_E_AC3;\n      case 0xA9:\n      case 0xAC:\n        return MimeTypes.AUDIO_DTS;\n      case 0xAA:\n      case 0xAB:\n        return MimeTypes.AUDIO_DTS_HD;\n      case 0xAD:\n        return MimeTypes.AUDIO_OPUS;\n      case 0xAE:\n        return MimeTypes.AUDIO_AC4;\n      default:\n        return null;\n    }\n  }\n\n  /**\n   * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type.\n   * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be\n   * established.\n   *\n   * @param mimeType The MIME type.\n   * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type.\n   */\n  public static int getTrackType(@Nullable String mimeType) {\n    if (TextUtils.isEmpty(mimeType)) {\n      return C.TRACK_TYPE_UNKNOWN;\n    } else if (isAudio(mimeType)) {\n      return C.TRACK_TYPE_AUDIO;\n    } else if (isVideo(mimeType)) {\n      return C.TRACK_TYPE_VIDEO;\n    } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType)\n        || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType)\n        || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType)\n        || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType)\n        || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType)\n        || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) {\n      return C.TRACK_TYPE_TEXT;\n    } else if (APPLICATION_ID3.equals(mimeType)\n        || APPLICATION_EMSG.equals(mimeType)\n        || APPLICATION_SCTE35.equals(mimeType)) {\n      return C.TRACK_TYPE_METADATA;\n    } else if (APPLICATION_CAMERA_MOTION.equals(mimeType)) {\n      return C.TRACK_TYPE_CAMERA_MOTION;\n    } else {\n      return getTrackTypeForCustomMimeType(mimeType);\n    }\n  }\n\n  /**\n   * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if\n   * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise.\n   *\n   * @param mimeType The MIME type.\n   * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or\n   *     {@link C#ENCODING_INVALID}.\n   */\n  public static @C.Encoding int getEncoding(String mimeType) {\n    switch (mimeType) {\n      case MimeTypes.AUDIO_AC3:\n        return C.ENCODING_AC3;\n      case MimeTypes.AUDIO_E_AC3:\n        return C.ENCODING_E_AC3;\n      case MimeTypes.AUDIO_E_AC3_JOC:\n        return C.ENCODING_E_AC3_JOC;\n      case MimeTypes.AUDIO_AC4:\n        return C.ENCODING_AC4;\n      case MimeTypes.AUDIO_DTS:\n        return C.ENCODING_DTS;\n      case MimeTypes.AUDIO_DTS_HD:\n        return C.ENCODING_DTS_HD;\n      case MimeTypes.AUDIO_TRUEHD:\n        return C.ENCODING_DOLBY_TRUEHD;\n      default:\n        return C.ENCODING_INVALID;\n    }\n  }\n\n  /**\n   * Equivalent to {@code getTrackType(getMediaMimeType(codec))}.\n   *\n   * @param codec The codec.\n   * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified codec.\n   */\n  public static int getTrackTypeOfCodec(String codec) {\n    return getTrackType(getMediaMimeType(codec));\n  }\n\n  /**\n   * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not\n   * contain a forward slash character ({@code '/'}).\n   */\n  private static @Nullable String getTopLevelType(@Nullable String mimeType) {\n    if (mimeType == null) {\n      return null;\n    }\n    int indexOfSlash = mimeType.indexOf('/');\n    if (indexOfSlash == -1) {\n      return null;\n    }\n    return mimeType.substring(0, indexOfSlash);\n  }\n\n  private static @Nullable String getCustomMimeTypeForCodec(String codec) {\n    int customMimeTypeCount = customMimeTypes.size();\n    for (int i = 0; i < customMimeTypeCount; i++) {\n      CustomMimeType customMimeType = customMimeTypes.get(i);\n      if (codec.startsWith(customMimeType.codecPrefix)) {\n        return customMimeType.mimeType;\n      }\n    }\n    return null;\n  }\n\n  private static int getTrackTypeForCustomMimeType(String mimeType) {\n    int customMimeTypeCount = customMimeTypes.size();\n    for (int i = 0; i < customMimeTypeCount; i++) {\n      CustomMimeType customMimeType = customMimeTypes.get(i);\n      if (mimeType.equals(customMimeType.mimeType)) {\n        return customMimeType.trackType;\n      }\n    }\n    return C.TRACK_TYPE_UNKNOWN;\n  }\n\n  private MimeTypes() {\n    // Prevent instantiation.\n  }\n\n  private static final class CustomMimeType {\n    public final String mimeType;\n    public final String codecPrefix;\n    public final int trackType;\n\n    public CustomMimeType(String mimeType, String codecPrefix, int trackType) {\n      this.mimeType = mimeType;\n      this.codecPrefix = codecPrefix;\n      this.trackType = trackType;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\n\n/**\n * Utility methods for handling H.264/AVC and H.265/HEVC NAL units.\n */\npublic final class NalUnitUtil {\n\n  private static final String TAG = \"NalUnitUtil\";\n\n  /**\n   * Holds data parsed from a sequence parameter set NAL unit.\n   */\n  public static final class SpsData {\n\n    public final int profileIdc;\n    public final int constraintsFlagsAndReservedZero2Bits;\n    public final int levelIdc;\n    public final int seqParameterSetId;\n    public final int width;\n    public final int height;\n    public final float pixelWidthAspectRatio;\n    public final boolean separateColorPlaneFlag;\n    public final boolean frameMbsOnlyFlag;\n    public final int frameNumLength;\n    public final int picOrderCountType;\n    public final int picOrderCntLsbLength;\n    public final boolean deltaPicOrderAlwaysZeroFlag;\n\n    public SpsData(\n        int profileIdc,\n        int constraintsFlagsAndReservedZero2Bits,\n        int levelIdc,\n        int seqParameterSetId,\n        int width,\n        int height,\n        float pixelWidthAspectRatio,\n        boolean separateColorPlaneFlag,\n        boolean frameMbsOnlyFlag,\n        int frameNumLength,\n        int picOrderCountType,\n        int picOrderCntLsbLength,\n        boolean deltaPicOrderAlwaysZeroFlag) {\n      this.profileIdc = profileIdc;\n      this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits;\n      this.levelIdc = levelIdc;\n      this.seqParameterSetId = seqParameterSetId;\n      this.width = width;\n      this.height = height;\n      this.pixelWidthAspectRatio = pixelWidthAspectRatio;\n      this.separateColorPlaneFlag = separateColorPlaneFlag;\n      this.frameMbsOnlyFlag = frameMbsOnlyFlag;\n      this.frameNumLength = frameNumLength;\n      this.picOrderCountType = picOrderCountType;\n      this.picOrderCntLsbLength = picOrderCntLsbLength;\n      this.deltaPicOrderAlwaysZeroFlag = deltaPicOrderAlwaysZeroFlag;\n    }\n\n  }\n\n  /**\n   * Holds data parsed from a picture parameter set NAL unit.\n   */\n  public static final class PpsData {\n\n    public final int picParameterSetId;\n    public final int seqParameterSetId;\n    public final boolean bottomFieldPicOrderInFramePresentFlag;\n\n    public PpsData(int picParameterSetId, int seqParameterSetId,\n        boolean bottomFieldPicOrderInFramePresentFlag) {\n      this.picParameterSetId = picParameterSetId;\n      this.seqParameterSetId = seqParameterSetId;\n      this.bottomFieldPicOrderInFramePresentFlag = bottomFieldPicOrderInFramePresentFlag;\n    }\n\n  }\n\n  /** Four initial bytes that must prefix NAL units for decoding. */\n  public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};\n\n  /** Value for aspect_ratio_idc indicating an extended aspect ratio, in H.264 and H.265 SPSs. */\n  public static final int EXTENDED_SAR = 0xFF;\n  /** Aspect ratios indexed by aspect_ratio_idc, in H.264 and H.265 SPSs. */\n  public static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {\n    1f /* Unspecified. Assume square */,\n    1f,\n    12f / 11f,\n    10f / 11f,\n    16f / 11f,\n    40f / 33f,\n    24f / 11f,\n    20f / 11f,\n    32f / 11f,\n    80f / 33f,\n    18f / 11f,\n    15f / 11f,\n    64f / 33f,\n    160f / 99f,\n    4f / 3f,\n    3f / 2f,\n    2f\n  };\n\n  private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information\n  private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set\n  private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39;\n\n  private static final Object scratchEscapePositionsLock = new Object();\n\n  /**\n   * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded\n   * by {@link #scratchEscapePositionsLock}.\n   */\n  private static int[] scratchEscapePositions = new int[10];\n\n  /**\n   * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with\n   * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.\n   * <p>\n   * Executions of this method are mutually exclusive, so it should not be called with very large\n   * buffers.\n   *\n   * @param data The data to unescape.\n   * @param limit The limit (exclusive) of the data to unescape.\n   * @return The length of the unescaped data.\n   */\n  public static int unescapeStream(byte[] data, int limit) {\n    synchronized (scratchEscapePositionsLock) {\n      int position = 0;\n      int scratchEscapeCount = 0;\n      while (position < limit) {\n        position = findNextUnescapeIndex(data, position, limit);\n        if (position < limit) {\n          if (scratchEscapePositions.length <= scratchEscapeCount) {\n            // Grow scratchEscapePositions to hold a larger number of positions.\n            scratchEscapePositions = Arrays.copyOf(scratchEscapePositions,\n                scratchEscapePositions.length * 2);\n          }\n          scratchEscapePositions[scratchEscapeCount++] = position;\n          position += 3;\n        }\n      }\n\n      int unescapedLength = limit - scratchEscapeCount;\n      int escapedPosition = 0; // The position being read from.\n      int unescapedPosition = 0; // The position being written to.\n      for (int i = 0; i < scratchEscapeCount; i++) {\n        int nextEscapePosition = scratchEscapePositions[i];\n        int copyLength = nextEscapePosition - escapedPosition;\n        System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength);\n        unescapedPosition += copyLength;\n        data[unescapedPosition++] = 0;\n        data[unescapedPosition++] = 0;\n        escapedPosition += copyLength + 3;\n      }\n\n      int remainingLength = unescapedLength - unescapedPosition;\n      System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength);\n      return unescapedLength;\n    }\n  }\n\n  /**\n   * Discards data from the buffer up to the first SPS, where {@code data.position()} is interpreted\n   * as the length of the buffer.\n   * <p>\n   * When the method returns, {@code data.position()} will contain the new length of the buffer. If\n   * the buffer is not empty it is guaranteed to start with an SPS.\n   *\n   * @param data Buffer containing start code delimited NAL units.\n   */\n  public static void discardToSps(ByteBuffer data) {\n    int length = data.position();\n    int consecutiveZeros = 0;\n    int offset = 0;\n    while (offset + 1 < length) {\n      int value = data.get(offset) & 0xFF;\n      if (consecutiveZeros == 3) {\n        if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) {\n          // Copy from this NAL unit onwards to the start of the buffer.\n          ByteBuffer offsetData = data.duplicate();\n          offsetData.position(offset - 3);\n          offsetData.limit(length);\n          data.position(0);\n          data.put(offsetData);\n          return;\n        }\n      } else if (value == 0) {\n        consecutiveZeros++;\n      }\n      if (value != 0) {\n        consecutiveZeros = 0;\n      }\n      offset++;\n    }\n    // Empty the buffer if the SPS NAL unit was not found.\n    data.clear();\n  }\n\n  /**\n   * Returns whether the NAL unit with the specified header contains supplemental enhancement\n   * information.\n   *\n   * @param mimeType The sample MIME type.\n   * @param nalUnitHeaderFirstByte The first byte of nal_unit().\n   * @return Whether the NAL unit with the specified header is an SEI NAL unit.\n   */\n  public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {\n    return (MimeTypes.VIDEO_H264.equals(mimeType)\n        && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)\n        || (MimeTypes.VIDEO_H265.equals(mimeType)\n        && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI);\n  }\n\n  /**\n   * Returns the type of the NAL unit in {@code data} that starts at {@code offset}.\n   *\n   * @param data The data to search.\n   * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and\n   *     {@code data.length - 3} (exclusive).\n   * @return The type of the unit.\n   */\n  public static int getNalUnitType(byte[] data, int offset) {\n    return data[offset + 3] & 0x1F;\n  }\n\n  /**\n   * Returns the type of the H.265 NAL unit in {@code data} that starts at {@code offset}.\n   *\n   * @param data The data to search.\n   * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and\n   *     {@code data.length - 3} (exclusive).\n   * @return The type of the unit.\n   */\n  public static int getH265NalUnitType(byte[] data, int offset) {\n    return (data[offset + 3] & 0x7E) >> 1;\n  }\n\n  /**\n   * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection\n   * 7.3.2.1.1.\n   *\n   * @param nalData A buffer containing escaped SPS data.\n   * @param nalOffset The offset of the NAL unit header in {@code nalData}.\n   * @param nalLimit The limit of the NAL unit in {@code nalData}.\n   * @return A parsed representation of the SPS data.\n   */\n  public static SpsData parseSpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {\n    ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);\n    data.skipBits(8); // nal_unit\n    int profileIdc = data.readBits(8);\n    int constraintsFlagsAndReservedZero2Bits = data.readBits(8);\n    int levelIdc = data.readBits(8);\n    int seqParameterSetId = data.readUnsignedExpGolombCodedInt();\n\n    int chromaFormatIdc = 1; // Default is 4:2:0\n    boolean separateColorPlaneFlag = false;\n    if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244\n        || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118\n        || profileIdc == 128 || profileIdc == 138) {\n      chromaFormatIdc = data.readUnsignedExpGolombCodedInt();\n      if (chromaFormatIdc == 3) {\n        separateColorPlaneFlag = data.readBit();\n      }\n      data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8\n      data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8\n      data.skipBit(); // qpprime_y_zero_transform_bypass_flag\n      boolean seqScalingMatrixPresentFlag = data.readBit();\n      if (seqScalingMatrixPresentFlag) {\n        int limit = (chromaFormatIdc != 3) ? 8 : 12;\n        for (int i = 0; i < limit; i++) {\n          boolean seqScalingListPresentFlag = data.readBit();\n          if (seqScalingListPresentFlag) {\n            skipScalingList(data, i < 6 ? 16 : 64);\n          }\n        }\n      }\n    }\n\n    int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4\n    int picOrderCntType = data.readUnsignedExpGolombCodedInt();\n    int picOrderCntLsbLength = 0;\n    boolean deltaPicOrderAlwaysZeroFlag = false;\n    if (picOrderCntType == 0) {\n      // log2_max_pic_order_cnt_lsb_minus4 + 4\n      picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;\n    } else if (picOrderCntType == 1) {\n      deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag\n      data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic\n      data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field\n      long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();\n      for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {\n        data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]\n      }\n    }\n    data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames\n    data.skipBit(); // gaps_in_frame_num_value_allowed_flag\n\n    int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;\n    int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;\n    boolean frameMbsOnlyFlag = data.readBit();\n    int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;\n    if (!frameMbsOnlyFlag) {\n      data.skipBit(); // mb_adaptive_frame_field_flag\n    }\n\n    data.skipBit(); // direct_8x8_inference_flag\n    int frameWidth = picWidthInMbs * 16;\n    int frameHeight = frameHeightInMbs * 16;\n    boolean frameCroppingFlag = data.readBit();\n    if (frameCroppingFlag) {\n      int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt();\n      int frameCropRightOffset = data.readUnsignedExpGolombCodedInt();\n      int frameCropTopOffset = data.readUnsignedExpGolombCodedInt();\n      int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt();\n      int cropUnitX;\n      int cropUnitY;\n      if (chromaFormatIdc == 0) {\n        cropUnitX = 1;\n        cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);\n      } else {\n        int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;\n        int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;\n        cropUnitX = subWidthC;\n        cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));\n      }\n      frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;\n      frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;\n    }\n\n    float pixelWidthHeightRatio = 1;\n    boolean vuiParametersPresentFlag = data.readBit();\n    if (vuiParametersPresentFlag) {\n      boolean aspectRatioInfoPresentFlag = data.readBit();\n      if (aspectRatioInfoPresentFlag) {\n        int aspectRatioIdc = data.readBits(8);\n        if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {\n          int sarWidth = data.readBits(16);\n          int sarHeight = data.readBits(16);\n          if (sarWidth != 0 && sarHeight != 0) {\n            pixelWidthHeightRatio = (float) sarWidth / sarHeight;\n          }\n        } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {\n          pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];\n        } else {\n          Log.w(TAG, \"Unexpected aspect_ratio_idc value: \" + aspectRatioIdc);\n        }\n      }\n    }\n\n    return new SpsData(\n        profileIdc,\n        constraintsFlagsAndReservedZero2Bits,\n        levelIdc,\n        seqParameterSetId,\n        frameWidth,\n        frameHeight,\n        pixelWidthHeightRatio,\n        separateColorPlaneFlag,\n        frameMbsOnlyFlag,\n        frameNumLength,\n        picOrderCntType,\n        picOrderCntLsbLength,\n        deltaPicOrderAlwaysZeroFlag);\n  }\n\n  /**\n   * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection\n   * 7.3.2.2.\n   *\n   * @param nalData A buffer containing escaped PPS data.\n   * @param nalOffset The offset of the NAL unit header in {@code nalData}.\n   * @param nalLimit The limit of the NAL unit in {@code nalData}.\n   * @return A parsed representation of the PPS data.\n   */\n  public static PpsData parsePpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {\n    ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);\n    data.skipBits(8); // nal_unit\n    int picParameterSetId = data.readUnsignedExpGolombCodedInt();\n    int seqParameterSetId = data.readUnsignedExpGolombCodedInt();\n    data.skipBit(); // entropy_coding_mode_flag\n    boolean bottomFieldPicOrderInFramePresentFlag = data.readBit();\n    return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag);\n  }\n\n  /**\n   * Finds the first NAL unit in {@code data}.\n   * <p>\n   * If {@code prefixFlags} is null then the first three bytes of a NAL unit must be entirely\n   * contained within the part of the array being searched in order for it to be found.\n   * <p>\n   * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four\n   * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same\n   * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables\n   * the detection of such NAL units. Note that when using this feature, the return value may be 3,\n   * 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before\n   * the first byte in the current array.\n   *\n   * @param data The data to search.\n   * @param startOffset The offset (inclusive) in the data to start the search.\n   * @param endOffset The offset (exclusive) in the data to end the search.\n   * @param prefixFlags A boolean array whose first three elements are used to store the state\n   *     required to detect NAL units where the NAL unit prefix spans array boundaries. The array\n   *     must be at least 3 elements long.\n   * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.\n   */\n  public static int findNalUnit(byte[] data, int startOffset, int endOffset,\n      boolean[] prefixFlags) {\n    int length = endOffset - startOffset;\n\n    Assertions.checkState(length >= 0);\n    if (length == 0) {\n      return endOffset;\n    }\n\n    if (prefixFlags != null) {\n      if (prefixFlags[0]) {\n        clearPrefixFlags(prefixFlags);\n        return startOffset - 3;\n      } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {\n        clearPrefixFlags(prefixFlags);\n        return startOffset - 2;\n      } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0\n          && data[startOffset + 1] == 1) {\n        clearPrefixFlags(prefixFlags);\n        return startOffset - 1;\n      }\n    }\n\n    int limit = endOffset - 1;\n    // We're looking for the NAL unit start code prefix 0x000001. The value of i tracks the index of\n    // the third byte.\n    for (int i = startOffset + 2; i < limit; i += 3) {\n      if ((data[i] & 0xFE) != 0) {\n        // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the\n        // loop advance the index by three.\n      } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {\n        if (prefixFlags != null) {\n          clearPrefixFlags(prefixFlags);\n        }\n        return i - 2;\n      } else {\n        // There isn't a NAL prefix here, but there might be at the next position. We should\n        // only skip forward by one. The loop will skip forward by three, so subtract two here.\n        i -= 2;\n      }\n    }\n\n    if (prefixFlags != null) {\n      // True if the last three bytes in the data seen so far are {0,0,1}.\n      prefixFlags[0] = length > 2\n          ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)\n          : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)\n          : (prefixFlags[1] && data[endOffset - 1] == 1);\n      // True if the last two bytes in the data seen so far are {0,0}.\n      prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0\n          : prefixFlags[2] && data[endOffset - 1] == 0;\n      // True if the last byte in the data seen so far is {0}.\n      prefixFlags[2] = data[endOffset - 1] == 0;\n    }\n\n    return endOffset;\n  }\n\n  /**\n   * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}.\n   *\n   * @param prefixFlags The flags to clear.\n   */\n  public static void clearPrefixFlags(boolean[] prefixFlags) {\n    prefixFlags[0] = false;\n    prefixFlags[1] = false;\n    prefixFlags[2] = false;\n  }\n\n  private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {\n    for (int i = offset; i < limit - 2; i++) {\n      if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {\n        return i;\n      }\n    }\n    return limit;\n  }\n\n  private static void skipScalingList(ParsableNalUnitBitArray bitArray, int size) {\n    int lastScale = 8;\n    int nextScale = 8;\n    for (int i = 0; i < size; i++) {\n      if (nextScale != 0) {\n        int deltaScale = bitArray.readSignedExpGolombCodedInt();\n        nextScale = (lastScale + deltaScale + 256) % 256;\n      }\n      lastScale = (nextScale == 0) ? lastScale : nextScale;\n    }\n  }\n\n  private NalUnitUtil() {\n    // Prevent instantiation.\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport javax.annotation.Nonnull;\nimport javax.annotation.meta.TypeQualifierDefault;\nimport kotlin.annotations.jvm.MigrationStatus;\nimport kotlin.annotations.jvm.UnderMigration;\n\n/**\n * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless\n * explicitly marked with a nullable annotation.\n */\n@Nonnull\n@TypeQualifierDefault(ElementType.TYPE_USE)\n@UnderMigration(status = MigrationStatus.STRICT)\n@Retention(RetentionPolicy.CLASS)\npublic @interface NonNullApi {}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.annotation.SuppressLint;\nimport android.app.Notification;\nimport android.app.NotificationChannel;\nimport android.app.NotificationManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Utility methods for displaying {@link Notification Notifications}. */\n@SuppressLint(\"InlinedApi\")\npublic final class NotificationUtil {\n\n  /**\n   * Notification channel importance levels. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link\n   * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link\n   * #IMPORTANCE_DEFAULT} or {@link #IMPORTANCE_HIGH}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    IMPORTANCE_UNSPECIFIED,\n    IMPORTANCE_NONE,\n    IMPORTANCE_MIN,\n    IMPORTANCE_LOW,\n    IMPORTANCE_DEFAULT,\n    IMPORTANCE_HIGH\n  })\n  public @interface Importance {}\n  /** @see NotificationManager#IMPORTANCE_UNSPECIFIED */\n  public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED;\n  /** @see NotificationManager#IMPORTANCE_NONE */\n  public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE;\n  /** @see NotificationManager#IMPORTANCE_MIN */\n  public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN;\n  /** @see NotificationManager#IMPORTANCE_LOW */\n  public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW;\n  /** @see NotificationManager#IMPORTANCE_DEFAULT */\n  public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT;\n  /** @see NotificationManager#IMPORTANCE_HIGH */\n  public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;\n\n  /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */\n  @Deprecated\n  public static void createNotificationChannel(\n      Context context, String id, @StringRes int nameResourceId, @Importance int importance) {\n    createNotificationChannel(\n        context, id, nameResourceId, /* descriptionResourceId= */ 0, importance);\n  }\n\n  /**\n   * Creates a notification channel that notifications can be posted to. See {@link\n   * NotificationChannel} and {@link\n   * NotificationManager#createNotificationChannel(NotificationChannel)} for details.\n   *\n   * @param context A {@link Context}.\n   * @param id The id of the channel. Must be unique per package. The value may be truncated if it's\n   *     too long.\n   * @param nameResourceId A string resource identifier for the user visible name of the channel.\n   *     The recommended maximum length is 40 characters. The string may be truncated if it's too\n   *     long. You can rename the channel when the system locale changes by listening for the {@link\n   *     Intent#ACTION_LOCALE_CHANGED} broadcast.\n   * @param descriptionResourceId A string resource identifier for the user visible description of\n   *     the channel, or 0 if no description is provided. The recommended maximum length is 300\n   *     characters. The value may be truncated if it is too long. You can change the description of\n   *     the channel when the system locale changes by listening for the {@link\n   *     Intent#ACTION_LOCALE_CHANGED} broadcast.\n   * @param importance The importance of the channel. This controls how interruptive notifications\n   *     posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link\n   *     #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link\n   *     #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}.\n   */\n  public static void createNotificationChannel(\n      Context context,\n      String id,\n      @StringRes int nameResourceId,\n      @StringRes int descriptionResourceId,\n      @Importance int importance) {\n    if (Util.SDK_INT >= 26) {\n      NotificationManager notificationManager =\n          (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\n      NotificationChannel channel =\n          new NotificationChannel(id, context.getString(nameResourceId), importance);\n      if (descriptionResourceId != 0) {\n        channel.setDescription(context.getString(descriptionResourceId));\n      }\n      notificationManager.createNotificationChannel(channel);\n    }\n  }\n\n  /**\n   * Post a notification to be shown in the status bar. If a notification with the same id has\n   * already been posted by your application and has not yet been canceled, it will be replaced by\n   * the updated information. If {@code notification} is {@code null} then any notification\n   * previously shown with the specified id will be cancelled.\n   *\n   * @param context A {@link Context}.\n   * @param id The notification id.\n   * @param notification The {@link Notification} to post, or {@code null} to cancel a previously\n   *     shown notification.\n   */\n  public static void setNotification(Context context, int id, @Nullable Notification notification) {\n    NotificationManager notificationManager =\n        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\n    if (notification != null) {\n      notificationManager.notify(id, notification);\n    } else {\n      notificationManager.cancel(id);\n    }\n  }\n\n  private NotificationUtil() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\n/**\n * Wraps a byte array, providing methods that allow it to be read as a bitstream.\n */\npublic final class ParsableBitArray {\n\n  public byte[] data;\n\n  // The offset within the data, stored as the current byte offset, and the bit offset within that\n  // byte (from 0 to 7).\n  private int byteOffset;\n  private int bitOffset;\n  private int byteLimit;\n\n  /** Creates a new instance that initially has no backing data. */\n  public ParsableBitArray() {\n    data = Util.EMPTY_BYTE_ARRAY;\n  }\n\n  /**\n   * Creates a new instance that wraps an existing array.\n   *\n   * @param data The data to wrap.\n   */\n  public ParsableBitArray(byte[] data) {\n    this(data, data.length);\n  }\n\n  /**\n   * Creates a new instance that wraps an existing array.\n   *\n   * @param data The data to wrap.\n   * @param limit The limit in bytes.\n   */\n  public ParsableBitArray(byte[] data, int limit) {\n    this.data = data;\n    byteLimit = limit;\n  }\n\n  /**\n   * Updates the instance to wrap {@code data}, and resets the position to zero.\n   *\n   * @param data The array to wrap.\n   */\n  public void reset(byte[] data) {\n    reset(data, data.length);\n  }\n\n  /**\n   * Sets this instance's data, position and limit to match the provided {@code parsableByteArray}.\n   * Any modifications to the underlying data array will be visible in both instances\n   *\n   * @param parsableByteArray The {@link ParsableByteArray}.\n   */\n  public void reset(ParsableByteArray parsableByteArray) {\n    reset(parsableByteArray.data, parsableByteArray.limit());\n    setPosition(parsableByteArray.getPosition() * 8);\n  }\n\n  /**\n   * Updates the instance to wrap {@code data}, and resets the position to zero.\n   *\n   * @param data The array to wrap.\n   * @param limit The limit in bytes.\n   */\n  public void reset(byte[] data, int limit) {\n    this.data = data;\n    byteOffset = 0;\n    bitOffset = 0;\n    byteLimit = limit;\n  }\n\n  /**\n   * Returns the number of bits yet to be read.\n   */\n  public int bitsLeft() {\n    return (byteLimit - byteOffset) * 8 - bitOffset;\n  }\n\n  /**\n   * Returns the current bit offset.\n   */\n  public int getPosition() {\n    return byteOffset * 8 + bitOffset;\n  }\n\n  /**\n   * Returns the current byte offset. Must only be called when the position is byte aligned.\n   *\n   * @throws IllegalStateException If the position isn't byte aligned.\n   */\n  public int getBytePosition() {\n    Assertions.checkState(bitOffset == 0);\n    return byteOffset;\n  }\n\n  /**\n   * Sets the current bit offset.\n   *\n   * @param position The position to set.\n   */\n  public void setPosition(int position) {\n    byteOffset = position / 8;\n    bitOffset = position - (byteOffset * 8);\n    assertValidOffset();\n  }\n\n  /**\n   * Skips a single bit.\n   */\n  public void skipBit() {\n    if (++bitOffset == 8) {\n      bitOffset = 0;\n      byteOffset++;\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Skips bits and moves current reading position forward.\n   *\n   * @param numBits The number of bits to skip.\n   */\n  public void skipBits(int numBits) {\n    int numBytes = numBits / 8;\n    byteOffset += numBytes;\n    bitOffset += numBits - (numBytes * 8);\n    if (bitOffset > 7) {\n      byteOffset++;\n      bitOffset -= 8;\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Reads a single bit.\n   *\n   * @return Whether the bit is set.\n   */\n  public boolean readBit() {\n    boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;\n    skipBit();\n    return returnValue;\n  }\n\n  /**\n   * Reads up to 32 bits.\n   *\n   * @param numBits The number of bits to read.\n   * @return An integer whose bottom {@code numBits} bits hold the read data.\n   */\n  public int readBits(int numBits) {\n    if (numBits == 0) {\n      return 0;\n    }\n    int returnValue = 0;\n    bitOffset += numBits;\n    while (bitOffset > 8) {\n      bitOffset -= 8;\n      returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;\n    }\n    returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);\n    returnValue &= 0xFFFFFFFF >>> (32 - numBits);\n    if (bitOffset == 8) {\n      bitOffset = 0;\n      byteOffset++;\n    }\n    assertValidOffset();\n    return returnValue;\n  }\n\n  /**\n   * Reads up to 64 bits.\n   *\n   * @param numBits The number of bits to read.\n   * @return A long whose bottom {@code numBits} bits hold the read data.\n   */\n  public long readBitsToLong(int numBits) {\n    if (numBits <= 32) {\n      return Util.toUnsignedLong(readBits(numBits));\n    }\n    return Util.toUnsignedLong(readBits(numBits - 32)) << 32 | Util.toUnsignedLong(readBits(32));\n  }\n\n  /**\n   * Reads {@code numBits} bits into {@code buffer}.\n   *\n   * @param buffer The array into which the read data should be written. The trailing {@code numBits\n   *     % 8} bits are written into the most significant bits of the last modified {@code buffer}\n   *     byte. The remaining ones are unmodified.\n   * @param offset The offset in {@code buffer} at which the read data should be written.\n   * @param numBits The number of bits to read.\n   */\n  public void readBits(byte[] buffer, int offset, int numBits) {\n    // Whole bytes.\n    int to = offset + (numBits >> 3) /* numBits / 8 */;\n    for (int i = offset; i < to; i++) {\n      buffer[i] = (byte) (data[byteOffset++] << bitOffset);\n      buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset)));\n    }\n    // Trailing bits.\n    int bitsLeft = numBits & 7 /* numBits % 8 */;\n    if (bitsLeft == 0) {\n      return;\n    }\n    // Set bits that are going to be overwritten to 0.\n    buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft));\n    if (bitOffset + bitsLeft > 8) {\n      // We read the rest of data[byteOffset] and increase byteOffset.\n      buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset));\n      bitOffset -= 8;\n    }\n    bitOffset += bitsLeft;\n    int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset);\n    buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft));\n    if (bitOffset == 8) {\n      bitOffset = 0;\n      byteOffset++;\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Aligns the position to the next byte boundary. Does nothing if the position is already aligned.\n   */\n  public void byteAlign() {\n    if (bitOffset == 0) {\n      return;\n    }\n    bitOffset = 0;\n    byteOffset++;\n    assertValidOffset();\n  }\n\n  /**\n   * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position\n   * is byte aligned.\n   *\n   * @see System#arraycopy(Object, int, Object, int, int)\n   * @param buffer The array into which the read data should be written.\n   * @param offset The offset in {@code buffer} at which the read data should be written.\n   * @param length The number of bytes to read.\n   * @throws IllegalStateException If the position isn't byte aligned.\n   */\n  public void readBytes(byte[] buffer, int offset, int length) {\n    Assertions.checkState(bitOffset == 0);\n    System.arraycopy(data, byteOffset, buffer, offset, length);\n    byteOffset += length;\n    assertValidOffset();\n  }\n\n  /**\n   * Skips the next {@code length} bytes. Must only be called when the position is byte aligned.\n   *\n   * @param length The number of bytes to read.\n   * @throws IllegalStateException If the position isn't byte aligned.\n   */\n  public void skipBytes(int length) {\n    Assertions.checkState(bitOffset == 0);\n    byteOffset += length;\n    assertValidOffset();\n  }\n\n  /**\n   * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits\n   * from {@code value}. Bits are written in order from most significant to least significant. The\n   * read position is advanced by {@code numBits}.\n   *\n   * @param value The integer whose {@code numBits} least significant bits are written into {@link\n   *     #data}.\n   * @param numBits The number of bits to write.\n   */\n  public void putInt(int value, int numBits) {\n    int remainingBitsToRead = numBits;\n    if (numBits < 32) {\n      value &= (1 << numBits) - 1;\n    }\n    int firstByteReadSize = Math.min(8 - bitOffset, numBits);\n    int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;\n    int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);\n    data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask);\n    int firstByteInputBits = value >>> (numBits - firstByteReadSize);\n    data[byteOffset] =\n        (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize));\n    remainingBitsToRead -= firstByteReadSize;\n    int currentByteIndex = byteOffset + 1;\n    while (remainingBitsToRead > 8) {\n      data[currentByteIndex++] = (byte) (value >>> (remainingBitsToRead - 8));\n      remainingBitsToRead -= 8;\n    }\n    int lastByteRightPaddingSize = 8 - remainingBitsToRead;\n    data[currentByteIndex] =\n        (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1));\n    int lastByteInput = value & ((1 << remainingBitsToRead) - 1);\n    data[currentByteIndex] =\n        (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize));\n    skipBits(numBits);\n    assertValidOffset();\n  }\n\n  private void assertValidOffset() {\n    // It is fine for position to be at the end of the array, but no further.\n    Assertions.checkState(byteOffset >= 0\n        && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\n\n/**\n * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are\n * parsed with the assumption that their constituent bytes are in big endian order.\n */\npublic final class ParsableByteArray {\n\n  public byte[] data;\n\n  private int position;\n  private int limit;\n\n  /** Creates a new instance that initially has no backing data. */\n  public ParsableByteArray() {\n    data = Util.EMPTY_BYTE_ARRAY;\n  }\n\n  /**\n   * Creates a new instance with {@code limit} bytes and sets the limit.\n   *\n   * @param limit The limit to set.\n   */\n  public ParsableByteArray(int limit) {\n    this.data = new byte[limit];\n    this.limit = limit;\n  }\n\n  /**\n   * Creates a new instance wrapping {@code data}, and sets the limit to {@code data.length}.\n   *\n   * @param data The array to wrap.\n   */\n  public ParsableByteArray(byte[] data) {\n    this.data = data;\n    limit = data.length;\n  }\n\n  /**\n   * Creates a new instance that wraps an existing array.\n   *\n   * @param data The data to wrap.\n   * @param limit The limit to set.\n   */\n  public ParsableByteArray(byte[] data, int limit) {\n    this.data = data;\n    this.limit = limit;\n  }\n\n  /** Sets the position and limit to zero. */\n  public void reset() {\n    position = 0;\n    limit = 0;\n  }\n\n  /**\n   * Resets the position to zero and the limit to the specified value. If the limit exceeds the\n   * capacity, {@code data} is replaced with a new array of sufficient size.\n   *\n   * @param limit The limit to set.\n   */\n  public void reset(int limit) {\n    reset(capacity() < limit ? new byte[limit] : data, limit);\n  }\n\n  /**\n   * Updates the instance to wrap {@code data}, and resets the position to zero and the limit to\n   * {@code data.length}.\n   *\n   * @param data The array to wrap.\n   */\n  public void reset(byte[] data) {\n    reset(data, data.length);\n  }\n\n  /**\n   * Updates the instance to wrap {@code data}, and resets the position to zero.\n   *\n   * @param data The array to wrap.\n   * @param limit The limit to set.\n   */\n  public void reset(byte[] data, int limit) {\n    this.data = data;\n    this.limit = limit;\n    position = 0;\n  }\n\n  /**\n   * Returns the number of bytes yet to be read.\n   */\n  public int bytesLeft() {\n    return limit - position;\n  }\n\n  /**\n   * Returns the limit.\n   */\n  public int limit() {\n    return limit;\n  }\n\n  /**\n   * Sets the limit.\n   *\n   * @param limit The limit to set.\n   */\n  public void setLimit(int limit) {\n    Assertions.checkArgument(limit >= 0 && limit <= data.length);\n    this.limit = limit;\n  }\n\n  /**\n   * Returns the current offset in the array, in bytes.\n   */\n  public int getPosition() {\n    return position;\n  }\n\n  /**\n   * Returns the capacity of the array, which may be larger than the limit.\n   */\n  public int capacity() {\n    return data.length;\n  }\n\n  /**\n   * Sets the reading offset in the array.\n   *\n   * @param position Byte offset in the array from which to read.\n   * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the\n   *     array.\n   */\n  public void setPosition(int position) {\n    // It is fine for position to be at the end of the array.\n    Assertions.checkArgument(position >= 0 && position <= limit);\n    this.position = position;\n  }\n\n  /**\n   * Moves the reading offset by {@code bytes}.\n   *\n   * @param bytes The number of bytes to skip.\n   * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the\n   *     array.\n   */\n  public void skipBytes(int bytes) {\n    setPosition(position + bytes);\n  }\n\n  /**\n   * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of\n   * {@code bitArray} to zero.\n   *\n   * @param bitArray The {@link ParsableBitArray} into which the bytes should be read.\n   * @param length The number of bytes to write.\n   */\n  public void readBytes(ParsableBitArray bitArray, int length) {\n    readBytes(bitArray.data, 0, length);\n    bitArray.setPosition(0);\n  }\n\n  /**\n   * Reads the next {@code length} bytes into {@code buffer} at {@code offset}.\n   *\n   * @see System#arraycopy(Object, int, Object, int, int)\n   * @param buffer The array into which the read data should be written.\n   * @param offset The offset in {@code buffer} at which the read data should be written.\n   * @param length The number of bytes to read.\n   */\n  public void readBytes(byte[] buffer, int offset, int length) {\n    System.arraycopy(data, position, buffer, offset, length);\n    position += length;\n  }\n\n  /**\n   * Reads the next {@code length} bytes into {@code buffer}.\n   *\n   * @see ByteBuffer#put(byte[], int, int)\n   * @param buffer The {@link ByteBuffer} into which the read data should be written.\n   * @param length The number of bytes to read.\n   */\n  public void readBytes(ByteBuffer buffer, int length) {\n    buffer.put(data, position, length);\n    position += length;\n  }\n\n  /**\n   * Peeks at the next byte as an unsigned value.\n   */\n  public int peekUnsignedByte() {\n    return (data[position] & 0xFF);\n  }\n\n  /**\n   * Peeks at the next char.\n   */\n  public char peekChar() {\n    return (char) ((data[position] & 0xFF) << 8\n        | (data[position + 1] & 0xFF));\n  }\n\n  /**\n   * Reads the next byte as an unsigned value.\n   */\n  public int readUnsignedByte() {\n    return (data[position++] & 0xFF);\n  }\n\n  /**\n   * Reads the next two bytes as an unsigned value.\n   */\n  public int readUnsignedShort() {\n    return (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF);\n  }\n\n  /**\n   * Reads the next two bytes as an unsigned value.\n   */\n  public int readLittleEndianUnsignedShort() {\n    return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8;\n  }\n\n  /**\n   * Reads the next two bytes as a signed value.\n   */\n  public short readShort() {\n    return (short) ((data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF));\n  }\n\n  /**\n   * Reads the next two bytes as a signed value.\n   */\n  public short readLittleEndianShort() {\n    return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8);\n  }\n\n  /**\n   * Reads the next three bytes as an unsigned value.\n   */\n  public int readUnsignedInt24() {\n    return (data[position++] & 0xFF) << 16\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF);\n  }\n\n  /**\n   * Reads the next three bytes as a signed value.\n   */\n  public int readInt24() {\n    return ((data[position++] & 0xFF) << 24) >> 8\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF);\n  }\n\n  /**\n   * Reads the next three bytes as a signed value in little endian order.\n   */\n  public int readLittleEndianInt24() {\n    return (data[position++] & 0xFF)\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF) << 16;\n  }\n\n  /**\n   * Reads the next three bytes as an unsigned value in little endian order.\n   */\n  public int readLittleEndianUnsignedInt24() {\n    return (data[position++] & 0xFF)\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF) << 16;\n  }\n\n  /**\n   * Reads the next four bytes as an unsigned value.\n   */\n  public long readUnsignedInt() {\n    return (data[position++] & 0xFFL) << 24\n        | (data[position++] & 0xFFL) << 16\n        | (data[position++] & 0xFFL) << 8\n        | (data[position++] & 0xFFL);\n  }\n\n  /**\n   * Reads the next four bytes as an unsigned value in little endian order.\n   */\n  public long readLittleEndianUnsignedInt() {\n    return (data[position++] & 0xFFL)\n        | (data[position++] & 0xFFL) << 8\n        | (data[position++] & 0xFFL) << 16\n        | (data[position++] & 0xFFL) << 24;\n  }\n\n  /**\n   * Reads the next four bytes as a signed value\n   */\n  public int readInt() {\n    return (data[position++] & 0xFF) << 24\n        | (data[position++] & 0xFF) << 16\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF);\n  }\n\n  /**\n   * Reads the next four bytes as a signed value in little endian order.\n   */\n  public int readLittleEndianInt() {\n    return (data[position++] & 0xFF)\n        | (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF) << 16\n        | (data[position++] & 0xFF) << 24;\n  }\n\n  /**\n   * Reads the next eight bytes as a signed value.\n   */\n  public long readLong() {\n    return (data[position++] & 0xFFL) << 56\n        | (data[position++] & 0xFFL) << 48\n        | (data[position++] & 0xFFL) << 40\n        | (data[position++] & 0xFFL) << 32\n        | (data[position++] & 0xFFL) << 24\n        | (data[position++] & 0xFFL) << 16\n        | (data[position++] & 0xFFL) << 8\n        | (data[position++] & 0xFFL);\n  }\n\n  /**\n   * Reads the next eight bytes as a signed value in little endian order.\n   */\n  public long readLittleEndianLong() {\n    return (data[position++] & 0xFFL)\n        | (data[position++] & 0xFFL) << 8\n        | (data[position++] & 0xFFL) << 16\n        | (data[position++] & 0xFFL) << 24\n        | (data[position++] & 0xFFL) << 32\n        | (data[position++] & 0xFFL) << 40\n        | (data[position++] & 0xFFL) << 48\n        | (data[position++] & 0xFFL) << 56;\n  }\n\n  /**\n   * Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer.\n   */\n  public int readUnsignedFixedPoint1616() {\n    int result = (data[position++] & 0xFF) << 8\n        | (data[position++] & 0xFF);\n    position += 2; // Skip the non-integer portion.\n    return result;\n  }\n\n  /**\n   * Reads a Synchsafe integer.\n   * <p>\n   * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can\n   * store 28 bits of information.\n   *\n   * @return The parsed value.\n   */\n  public int readSynchSafeInt() {\n    int b1 = readUnsignedByte();\n    int b2 = readUnsignedByte();\n    int b3 = readUnsignedByte();\n    int b4 = readUnsignedByte();\n    return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;\n  }\n\n  /**\n   * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero.\n   *\n   * @throws IllegalStateException Thrown if the top bit of the input data is set.\n   */\n  public int readUnsignedIntToInt() {\n    int result = readInt();\n    if (result < 0) {\n      throw new IllegalStateException(\"Top bit not zero: \" + result);\n    }\n    return result;\n  }\n\n  /**\n   * Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit\n   * is a zero.\n   *\n   * @throws IllegalStateException Thrown if the top bit of the input data is set.\n   */\n  public int readLittleEndianUnsignedIntToInt() {\n    int result = readLittleEndianInt();\n    if (result < 0) {\n      throw new IllegalStateException(\"Top bit not zero: \" + result);\n    }\n    return result;\n  }\n\n  /**\n   * Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero.\n   *\n   * @throws IllegalStateException Thrown if the top bit of the input data is set.\n   */\n  public long readUnsignedLongToLong() {\n    long result = readLong();\n    if (result < 0) {\n      throw new IllegalStateException(\"Top bit not zero: \" + result);\n    }\n    return result;\n  }\n\n  /**\n   * Reads the next four bytes as a 32-bit floating point value.\n   */\n  public float readFloat() {\n    return Float.intBitsToFloat(readInt());\n  }\n\n  /**\n   * Reads the next eight bytes as a 64-bit floating point value.\n   */\n  public double readDouble() {\n    return Double.longBitsToDouble(readLong());\n  }\n\n  /**\n   * Reads the next {@code length} bytes as UTF-8 characters.\n   *\n   * @param length The number of bytes to read.\n   * @return The string encoded by the bytes.\n   */\n  public String readString(int length) {\n    return readString(length, Charset.forName(C.UTF8_NAME));\n  }\n\n  /**\n   * Reads the next {@code length} bytes as characters in the specified {@link Charset}.\n   *\n   * @param length The number of bytes to read.\n   * @param charset The character set of the encoded characters.\n   * @return The string encoded by the bytes in the specified character set.\n   */\n  public String readString(int length, Charset charset) {\n    String result = new String(data, position, length, charset);\n    position += length;\n    return result;\n  }\n\n  /**\n   * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is discarded,\n   * if present.\n   *\n   * @param length The number of bytes to read.\n   * @return The string, not including any terminating NUL byte.\n   */\n  public String readNullTerminatedString(int length) {\n    if (length == 0) {\n      return \"\";\n    }\n    int stringLength = length;\n    int lastIndex = position + length - 1;\n    if (lastIndex < limit && data[lastIndex] == 0) {\n      stringLength--;\n    }\n    String result = Util.fromUtf8Bytes(data, position, stringLength);\n    position += length;\n    return result;\n  }\n\n  /**\n   * Reads up to the next NUL byte (or the limit) as UTF-8 characters.\n   *\n   * @return The string not including any terminating NUL byte, or null if the end of the data has\n   *     already been reached.\n   */\n  @Nullable\n  public String readNullTerminatedString() {\n    if (bytesLeft() == 0) {\n      return null;\n    }\n    int stringLimit = position;\n    while (stringLimit < limit && data[stringLimit] != 0) {\n      stringLimit++;\n    }\n    String string = Util.fromUtf8Bytes(data, position, stringLimit - position);\n    position = stringLimit;\n    if (position < limit) {\n      position++;\n    }\n    return string;\n  }\n\n  /**\n   * Reads a line of text.\n   *\n   * <p>A line is considered to be terminated by any one of a carriage return ('\\r'), a line feed\n   * ('\\n'), or a carriage return followed immediately by a line feed ('\\r\\n'). The system's default\n   * charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present.\n   *\n   * @return The line not including any line-termination characters, or null if the end of the data\n   *     has already been reached.\n   */\n  @Nullable\n  public String readLine() {\n    if (bytesLeft() == 0) {\n      return null;\n    }\n    int lineLimit = position;\n    while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {\n      lineLimit++;\n    }\n    if (lineLimit - position >= 3 && data[position] == (byte) 0xEF\n        && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) {\n      // There's a UTF-8 byte order mark at the start of the line. Discard it.\n      position += 3;\n    }\n    String line = Util.fromUtf8Bytes(data, position, lineLimit - position);\n    position = lineLimit;\n    if (position == limit) {\n      return line;\n    }\n    if (data[position] == '\\r') {\n      position++;\n      if (position == limit) {\n        return line;\n      }\n    }\n    if (data[position] == '\\n') {\n      position++;\n    }\n    return line;\n  }\n\n  /**\n   * Reads a long value encoded by UTF-8 encoding\n   *\n   * @throws NumberFormatException if there is a problem with decoding\n   * @return Decoded long value\n   */\n  public long readUtf8EncodedLong() {\n    int length = 0;\n    long value = data[position];\n    // find the high most 0 bit\n    for (int j = 7; j >= 0; j--) {\n      if ((value & (1 << j)) == 0) {\n        if (j < 6) {\n          value &= (1 << j) - 1;\n          length = 7 - j;\n        } else if (j == 7) {\n          length = 1;\n        }\n        break;\n      }\n    }\n    if (length == 0) {\n      throw new NumberFormatException(\"Invalid UTF-8 sequence first byte: \" + value);\n    }\n    for (int i = 1; i < length; i++) {\n      int x = data[position + i];\n      if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th\n        throw new NumberFormatException(\"Invalid UTF-8 sequence continuation byte: \" + value);\n      }\n      value = (value << 6) | (x & 0x3F);\n    }\n    position += length;\n    return value;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\n/**\n * Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream.\n * <p>\n * Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0, 0]\n * for all reading/skipping operations, which makes the bitstream appear to be unescaped.\n */\npublic final class ParsableNalUnitBitArray {\n\n  private byte[] data;\n  private int byteLimit;\n\n  // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3].\n  private int byteOffset;\n  private int bitOffset;\n\n  /**\n   * @param data The data to wrap.\n   * @param offset The byte offset in {@code data} to start reading from.\n   * @param limit The byte offset of the end of the bitstream in {@code data}.\n   */\n  @SuppressWarnings({\"initialization.fields.uninitialized\", \"method.invocation.invalid\"})\n  public ParsableNalUnitBitArray(byte[] data, int offset, int limit) {\n    reset(data, offset, limit);\n  }\n\n  /**\n   * Resets the wrapped data, limit and offset.\n   *\n   * @param data The data to wrap.\n   * @param offset The byte offset in {@code data} to start reading from.\n   * @param limit The byte offset of the end of the bitstream in {@code data}.\n   */\n  public void reset(byte[] data, int offset, int limit) {\n    this.data = data;\n    byteOffset = offset;\n    byteLimit = limit;\n    bitOffset = 0;\n    assertValidOffset();\n  }\n\n  /**\n   * Skips a single bit.\n   */\n  public void skipBit() {\n    if (++bitOffset == 8) {\n      bitOffset = 0;\n      byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Skips bits and moves current reading position forward.\n   *\n   * @param numBits The number of bits to skip.\n   */\n  public void skipBits(int numBits) {\n    int oldByteOffset = byteOffset;\n    int numBytes = numBits / 8;\n    byteOffset += numBytes;\n    bitOffset += numBits - (numBytes * 8);\n    if (bitOffset > 7) {\n      byteOffset++;\n      bitOffset -= 8;\n    }\n    for (int i = oldByteOffset + 1; i <= byteOffset; i++) {\n      if (shouldSkipByte(i)) {\n        // Skip the byte and move forward to check three bytes ahead.\n        byteOffset++;\n        i += 2;\n      }\n    }\n    assertValidOffset();\n  }\n\n  /**\n   * Returns whether it's possible to read {@code n} bits starting from the current offset. The\n   * offset is not modified.\n   *\n   * @param numBits The number of bits.\n   * @return Whether it is possible to read {@code n} bits.\n   */\n  public boolean canReadBits(int numBits) {\n    int oldByteOffset = byteOffset;\n    int numBytes = numBits / 8;\n    int newByteOffset = byteOffset + numBytes;\n    int newBitOffset = bitOffset + numBits - (numBytes * 8);\n    if (newBitOffset > 7) {\n      newByteOffset++;\n      newBitOffset -= 8;\n    }\n    for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) {\n      if (shouldSkipByte(i)) {\n        // Skip the byte and move forward to check three bytes ahead.\n        newByteOffset++;\n        i += 2;\n      }\n    }\n    return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0);\n  }\n\n  /**\n   * Reads a single bit.\n   *\n   * @return Whether the bit is set.\n   */\n  public boolean readBit() {\n    boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;\n    skipBit();\n    return returnValue;\n  }\n\n  /**\n   * Reads up to 32 bits.\n   *\n   * @param numBits The number of bits to read.\n   * @return An integer whose bottom n bits hold the read data.\n   */\n  public int readBits(int numBits) {\n    int returnValue = 0;\n    bitOffset += numBits;\n    while (bitOffset > 8) {\n      bitOffset -= 8;\n      returnValue |= (data[byteOffset] & 0xFF) << bitOffset;\n      byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;\n    }\n    returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);\n    returnValue &= 0xFFFFFFFF >>> (32 - numBits);\n    if (bitOffset == 8) {\n      bitOffset = 0;\n      byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;\n    }\n    assertValidOffset();\n    return returnValue;\n  }\n\n  /**\n   * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current\n   * offset. The offset is not modified.\n   *\n   * @return Whether it is possible to read an Exp-Golomb-coded integer.\n   */\n  public boolean canReadExpGolombCodedNum() {\n    int initialByteOffset = byteOffset;\n    int initialBitOffset = bitOffset;\n    int leadingZeros = 0;\n    while (byteOffset < byteLimit && !readBit()) {\n      leadingZeros++;\n    }\n    boolean hitLimit = byteOffset == byteLimit;\n    byteOffset = initialByteOffset;\n    bitOffset = initialBitOffset;\n    return !hitLimit && canReadBits(leadingZeros * 2 + 1);\n  }\n\n  /**\n   * Reads an unsigned Exp-Golomb-coded format integer.\n   *\n   * @return The value of the parsed Exp-Golomb-coded integer.\n   */\n  public int readUnsignedExpGolombCodedInt() {\n    return readExpGolombCodeNum();\n  }\n\n  /**\n   * Reads an signed Exp-Golomb-coded format integer.\n   *\n   * @return The value of the parsed Exp-Golomb-coded integer.\n   */\n  public int readSignedExpGolombCodedInt() {\n    int codeNum = readExpGolombCodeNum();\n    return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);\n  }\n\n  private int readExpGolombCodeNum() {\n    int leadingZeros = 0;\n    while (!readBit()) {\n      leadingZeros++;\n    }\n    return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);\n  }\n\n  private boolean shouldSkipByte(int offset) {\n    return 2 <= offset && offset < byteLimit && data[offset] == (byte) 0x03\n        && data[offset - 2] == (byte) 0x00 && data[offset - 1] == (byte) 0x00;\n  }\n\n  private void assertValidOffset() {\n    // It is fine for position to be at the end of the array, but no further.\n    Assertions.checkState(byteOffset >= 0\n        && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/Predicate.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\n/**\n * Determines a true or false value for a given input.\n *\n * @param <T> The input type of the predicate.\n */\npublic interface Predicate<T> {\n\n  /**\n   * Evaluates an input.\n   *\n   * @param input The input to evaluate.\n   * @return The evaluated result.\n   */\n  boolean evaluate(T input);\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.PriorityQueue;\n\n/**\n * Allows tasks with associated priorities to control how they proceed relative to one another.\n * <p>\n * A task should call {@link #add(int)} to register with the manager and {@link #remove(int)} to\n * unregister. A registered task will prevent tasks of lower priority from proceeding, and should\n * call {@link #proceed(int)}, {@link #proceedNonBlocking(int)} or {@link #proceedOrThrow(int)} each\n * time it wishes to check whether it is itself allowed to proceed.\n */\npublic final class PriorityTaskManager {\n\n  /**\n   * Thrown when task attempts to proceed when another registered task has a higher priority.\n   */\n  public static class PriorityTooLowException extends IOException {\n\n    public PriorityTooLowException(int priority, int highestPriority) {\n      super(\"Priority too low [priority=\" + priority + \", highest=\" + highestPriority + \"]\");\n    }\n\n  }\n\n  private final Object lock = new Object();\n\n  // Guarded by lock.\n  private final PriorityQueue<Integer> queue;\n  private int highestPriority;\n\n  public PriorityTaskManager() {\n    queue = new PriorityQueue<>(10, Collections.reverseOrder());\n    highestPriority = Integer.MIN_VALUE;\n  }\n\n  /**\n   * Register a new task. The task must call {@link #remove(int)} when done.\n   *\n   * @param priority The priority of the task. Larger values indicate higher priorities.\n   */\n  public void add(int priority) {\n    synchronized (lock) {\n      queue.add(priority);\n      highestPriority = Math.max(highestPriority, priority);\n    }\n  }\n\n  /**\n   * Blocks until the task is allowed to proceed.\n   *\n   * @param priority The priority of the task.\n   * @throws InterruptedException If the thread is interrupted.\n   */\n  public void proceed(int priority) throws InterruptedException {\n    synchronized (lock) {\n      while (highestPriority != priority) {\n        lock.wait();\n      }\n    }\n  }\n\n  /**\n   * A non-blocking variant of {@link #proceed(int)}.\n   *\n   * @param priority The priority of the task.\n   * @return Whether the task is allowed to proceed.\n   */\n  public boolean proceedNonBlocking(int priority) {\n    synchronized (lock) {\n      return highestPriority == priority;\n    }\n  }\n\n  /**\n   * A throwing variant of {@link #proceed(int)}.\n   *\n   * @param priority The priority of the task.\n   * @throws PriorityTooLowException If the task is not allowed to proceed.\n   */\n  public void proceedOrThrow(int priority) throws PriorityTooLowException {\n    synchronized (lock) {\n      if (highestPriority != priority) {\n        throw new PriorityTooLowException(priority, highestPriority);\n      }\n    }\n  }\n\n  /**\n   * Unregister a task.\n   *\n   * @param priority The priority of the task.\n   */\n  public void remove(int priority) {\n    synchronized (lock) {\n      queue.remove(priority);\n      highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : Util.castNonNull(queue.peek());\n      lock.notifyAll();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.Player;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Util class for repeat mode handling.\n */\npublic final class RepeatModeUtil {\n\n  // LINT.IfChange\n  /**\n   * Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are\n   * {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link\n   * #REPEAT_TOGGLE_MODE_ALL}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, REPEAT_TOGGLE_MODE_ALL})\n  public @interface RepeatToggleModes {}\n  /**\n   * All repeat mode buttons disabled.\n   */\n  public static final int REPEAT_TOGGLE_MODE_NONE = 0;\n  /**\n   * \"Repeat One\" button enabled.\n   */\n  public static final int REPEAT_TOGGLE_MODE_ONE = 1;\n  /** \"Repeat All\" button enabled. */\n  public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2\n  // LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)\n\n  private RepeatModeUtil() {\n    // Prevent instantiation.\n  }\n\n  /**\n   * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}.\n   *\n   * @param currentMode The current repeat mode.\n   * @param enabledModes Bitmask of enabled modes.\n   * @return The next repeat mode.\n   */\n  public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode,\n      int enabledModes) {\n    for (int offset = 1; offset <= 2; offset++) {\n      @Player.RepeatMode int proposedMode = (currentMode + offset) % 3;\n      if (isRepeatModeEnabled(proposedMode, enabledModes)) {\n        return proposedMode;\n      }\n    }\n    return currentMode;\n  }\n\n  /**\n   * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}.\n   *\n   * @param repeatMode The mode to check.\n   * @param enabledModes The bitmask representing the enabled modes.\n   * @return {@code true} if enabled.\n   */\n  public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) {\n    switch (repeatMode) {\n      case Player.REPEAT_MODE_OFF:\n        return true;\n      case Player.REPEAT_MODE_ONE:\n        return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0;\n      case Player.REPEAT_MODE_ALL:\n        return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0;\n      default:\n        return false;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.io.BufferedOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * This is a subclass of {@link BufferedOutputStream} with a {@link #reset(OutputStream)} method\n * that allows an instance to be re-used with another underlying output stream.\n */\npublic final class ReusableBufferedOutputStream extends BufferedOutputStream {\n\n  private boolean closed;\n\n  public ReusableBufferedOutputStream(OutputStream out) {\n    super(out);\n  }\n\n  public ReusableBufferedOutputStream(OutputStream out, int size) {\n    super(out, size);\n  }\n\n  @Override\n  public void close() throws IOException {\n    closed = true;\n\n    Throwable thrown = null;\n    try {\n      flush();\n    } catch (Throwable e) {\n      thrown = e;\n    }\n    try {\n      out.close();\n    } catch (Throwable e) {\n      if (thrown == null) {\n        thrown = e;\n      }\n    }\n    if (thrown != null) {\n      Util.sneakyThrow(thrown);\n    }\n  }\n\n  /**\n   * Resets this stream and uses the given output stream for writing. This stream must be closed\n   * before resetting.\n   *\n   * @param out New output stream to be used for writing.\n   * @throws IllegalStateException If the stream isn't closed.\n   */\n  public void reset(OutputStream out) {\n    Assertions.checkState(closed);\n    this.out = out;\n    count = 0;\n    closed = false;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\n\n/**\n * Calculate any percentile over a sliding window of weighted values. A maximum weight is\n * configured. Once the total weight of the values reaches the maximum weight, the oldest value is\n * reduced in weight until it reaches zero and is removed. This maintains a constant total weight,\n * equal to the maximum allowed, at the steady state.\n * <p>\n * This class can be used for bandwidth estimation based on a sliding window of past transfer rate\n * observations. This is an alternative to sliding mean and exponential averaging which suffer from\n * susceptibility to outliers and slow adaptation to step functions.\n *\n * @see <a href=\"http://en.wikipedia.org/wiki/Moving_average\">Wiki: Moving average</a>\n * @see <a href=\"http://en.wikipedia.org/wiki/Selection_algorithm\">Wiki: Selection algorithm</a>\n */\npublic class SlidingPercentile {\n\n  // Orderings.\n  private static final Comparator<Sample> INDEX_COMPARATOR = (a, b) -> a.index - b.index;\n  private static final Comparator<Sample> VALUE_COMPARATOR =\n      (a, b) -> Float.compare(a.value, b.value);\n\n  private static final int SORT_ORDER_NONE = -1;\n  private static final int SORT_ORDER_BY_VALUE = 0;\n  private static final int SORT_ORDER_BY_INDEX = 1;\n\n  private static final int MAX_RECYCLED_SAMPLES = 5;\n\n  private final int maxWeight;\n  private final ArrayList<Sample> samples;\n\n  private final Sample[] recycledSamples;\n\n  private int currentSortOrder;\n  private int nextSampleIndex;\n  private int totalWeight;\n  private int recycledSampleCount;\n\n  /**\n   * @param maxWeight The maximum weight.\n   */\n  public SlidingPercentile(int maxWeight) {\n    this.maxWeight = maxWeight;\n    recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];\n    samples = new ArrayList<>();\n    currentSortOrder = SORT_ORDER_NONE;\n  }\n\n  /** Resets the sliding percentile. */\n  public void reset() {\n    samples.clear();\n    currentSortOrder = SORT_ORDER_NONE;\n    nextSampleIndex = 0;\n    totalWeight = 0;\n  }\n\n  /**\n   * Adds a new weighted value.\n   *\n   * @param weight The weight of the new observation.\n   * @param value The value of the new observation.\n   */\n  public void addSample(int weight, float value) {\n    ensureSortedByIndex();\n\n    Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]\n        : new Sample();\n    newSample.index = nextSampleIndex++;\n    newSample.weight = weight;\n    newSample.value = value;\n    samples.add(newSample);\n    totalWeight += weight;\n\n    while (totalWeight > maxWeight) {\n      int excessWeight = totalWeight - maxWeight;\n      Sample oldestSample = samples.get(0);\n      if (oldestSample.weight <= excessWeight) {\n        totalWeight -= oldestSample.weight;\n        samples.remove(0);\n        if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {\n          recycledSamples[recycledSampleCount++] = oldestSample;\n        }\n      } else {\n        oldestSample.weight -= excessWeight;\n        totalWeight -= excessWeight;\n      }\n    }\n  }\n\n  /**\n   * Computes a percentile by integration.\n   *\n   * @param percentile The desired percentile, expressed as a fraction in the range (0,1].\n   * @return The requested percentile value or {@link Float#NaN} if no samples have been added.\n   */\n  public float getPercentile(float percentile) {\n    ensureSortedByValue();\n    float desiredWeight = percentile * totalWeight;\n    int accumulatedWeight = 0;\n    for (int i = 0; i < samples.size(); i++) {\n      Sample currentSample = samples.get(i);\n      accumulatedWeight += currentSample.weight;\n      if (accumulatedWeight >= desiredWeight) {\n        return currentSample.value;\n      }\n    }\n    // Clamp to maximum value or NaN if no values.\n    return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;\n  }\n\n  /**\n   * Sorts the samples by index.\n   */\n  private void ensureSortedByIndex() {\n    if (currentSortOrder != SORT_ORDER_BY_INDEX) {\n      Collections.sort(samples, INDEX_COMPARATOR);\n      currentSortOrder = SORT_ORDER_BY_INDEX;\n    }\n  }\n\n  /**\n   * Sorts the samples by value.\n   */\n  private void ensureSortedByValue() {\n    if (currentSortOrder != SORT_ORDER_BY_VALUE) {\n      Collections.sort(samples, VALUE_COMPARATOR);\n      currentSortOrder = SORT_ORDER_BY_VALUE;\n    }\n  }\n\n  private static class Sample {\n\n    public int index;\n    public int weight;\n    public float value;\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.PlaybackParameters;\n\n/**\n * A {@link MediaClock} whose position advances with real time based on the playback parameters when\n * started.\n */\npublic final class StandaloneMediaClock implements MediaClock {\n\n  private final Clock clock;\n\n  private boolean started;\n  private long baseUs;\n  private long baseElapsedMs;\n  private PlaybackParameters playbackParameters;\n\n  /**\n   * Creates a new standalone media clock using the given {@link Clock} implementation.\n   *\n   * @param clock A {@link Clock}.\n   */\n  public StandaloneMediaClock(Clock clock) {\n    this.clock = clock;\n    this.playbackParameters = PlaybackParameters.DEFAULT;\n  }\n\n  /**\n   * Starts the clock. Does nothing if the clock is already started.\n   */\n  public void start() {\n    if (!started) {\n      baseElapsedMs = clock.elapsedRealtime();\n      started = true;\n    }\n  }\n\n  /**\n   * Stops the clock. Does nothing if the clock is already stopped.\n   */\n  public void stop() {\n    if (started) {\n      resetPosition(getPositionUs());\n      started = false;\n    }\n  }\n\n  /**\n   * Resets the clock's position.\n   *\n   * @param positionUs The position to set in microseconds.\n   */\n  public void resetPosition(long positionUs) {\n    baseUs = positionUs;\n    if (started) {\n      baseElapsedMs = clock.elapsedRealtime();\n    }\n  }\n\n  @Override\n  public long getPositionUs() {\n    long positionUs = baseUs;\n    if (started) {\n      long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;\n      if (playbackParameters.speed == 1f) {\n        positionUs += C.msToUs(elapsedSinceBaseMs);\n      } else {\n        positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);\n      }\n    }\n    return positionUs;\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    // Store the current position as the new base, in case the playback speed has changed.\n    if (started) {\n      resetPosition(getPositionUs());\n    }\n    this.playbackParameters = playbackParameters;\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return playbackParameters;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/SystemClock.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Handler;\nimport android.os.Handler.Callback;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\n\n/**\n * The standard implementation of {@link Clock}.\n */\n/* package */ final class SystemClock implements Clock {\n\n  @Override\n  public long elapsedRealtime() {\n    return android.os.SystemClock.elapsedRealtime();\n  }\n\n  @Override\n  public long uptimeMillis() {\n    return android.os.SystemClock.uptimeMillis();\n  }\n\n  @Override\n  public void sleep(long sleepTimeMs) {\n    android.os.SystemClock.sleep(sleepTimeMs);\n  }\n\n  @Override\n  public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) {\n    return new SystemHandlerWrapper(new Handler(looper, callback));\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.os.Looper;\nimport android.os.Message;\nimport androidx.annotation.Nullable;\n\n/** The standard implementation of {@link HandlerWrapper}. */\n/* package */ final class SystemHandlerWrapper implements HandlerWrapper {\n\n  private final android.os.Handler handler;\n\n  public SystemHandlerWrapper(android.os.Handler handler) {\n    this.handler = handler;\n  }\n\n  @Override\n  public Looper getLooper() {\n    return handler.getLooper();\n  }\n\n  @Override\n  public Message obtainMessage(int what) {\n    return handler.obtainMessage(what);\n  }\n\n  @Override\n  public Message obtainMessage(int what, @Nullable Object obj) {\n    return handler.obtainMessage(what, obj);\n  }\n\n  @Override\n  public Message obtainMessage(int what, int arg1, int arg2) {\n    return handler.obtainMessage(what, arg1, arg2);\n  }\n\n  @Override\n  public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) {\n    return handler.obtainMessage(what, arg1, arg2, obj);\n  }\n\n  @Override\n  public boolean sendEmptyMessage(int what) {\n    return handler.sendEmptyMessage(what);\n  }\n\n  @Override\n  public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {\n    return handler.sendEmptyMessageAtTime(what, uptimeMs);\n  }\n\n  @Override\n  public void removeMessages(int what) {\n    handler.removeMessages(what);\n  }\n\n  @Override\n  public void removeCallbacksAndMessages(@Nullable Object token) {\n    handler.removeCallbacksAndMessages(token);\n  }\n\n  @Override\n  public boolean post(Runnable runnable) {\n    return handler.post(runnable);\n  }\n\n  @Override\n  public boolean postDelayed(Runnable runnable, long delayMs) {\n    return handler.postDelayed(runnable, delayMs);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport androidx.annotation.Nullable;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\n\n/** A utility class to keep a queue of values with timestamps. This class is thread safe. */\npublic final class TimedValueQueue<V> {\n  private static final int INITIAL_BUFFER_SIZE = 10;\n\n  // Looping buffer for timestamps and values\n  private long[] timestamps;\n  private @NullableType V[] values;\n  private int first;\n  private int size;\n\n  public TimedValueQueue() {\n    this(INITIAL_BUFFER_SIZE);\n  }\n\n  /** Creates a TimedValueBuffer with the given initial buffer size. */\n  public TimedValueQueue(int initialBufferSize) {\n    timestamps = new long[initialBufferSize];\n    values = newArray(initialBufferSize);\n  }\n\n  /**\n   * Associates the specified value with the specified timestamp. All new values should have a\n   * greater timestamp than the previously added values. Otherwise all values are removed before\n   * adding the new one.\n   */\n  public synchronized void add(long timestamp, V value) {\n    clearBufferOnTimeDiscontinuity(timestamp);\n    doubleCapacityIfFull();\n    addUnchecked(timestamp, value);\n  }\n\n  /** Removes all of the values. */\n  public synchronized void clear() {\n    first = 0;\n    size = 0;\n    Arrays.fill(values, null);\n  }\n\n  /** Returns number of the values buffered. */\n  public synchronized int size() {\n    return size;\n  }\n\n  /**\n   * Returns the value with the greatest timestamp which is less than or equal to the given\n   * timestamp. Removes all older values and the returned one from the buffer.\n   *\n   * @param timestamp The timestamp value.\n   * @return The value with the greatest timestamp which is less than or equal to the given\n   *     timestamp or null if there is no such value.\n   * @see #poll(long)\n   */\n  public synchronized @Nullable V pollFloor(long timestamp) {\n    return poll(timestamp, /* onlyOlder= */ true);\n  }\n\n  /**\n   * Returns the value with the closest timestamp to the given timestamp. Removes all older values\n   * including the returned one from the buffer.\n   *\n   * @param timestamp The timestamp value.\n   * @return The value with the closest timestamp or null if the buffer is empty.\n   * @see #pollFloor(long)\n   */\n  public synchronized @Nullable V poll(long timestamp) {\n    return poll(timestamp, /* onlyOlder= */ false);\n  }\n\n  /**\n   * Returns the value with the closest timestamp to the given timestamp. Removes all older values\n   * including the returned one from the buffer.\n   *\n   * @param timestamp The timestamp value.\n   * @param onlyOlder Whether this method can return a new value in case its timestamp value is\n   *     closest to {@code timestamp}.\n   * @return The value with the closest timestamp or null if the buffer is empty or there is no\n   *     older value and {@code onlyOlder} is true.\n   */\n  @Nullable\n  private V poll(long timestamp, boolean onlyOlder) {\n    V value = null;\n    long previousTimeDiff = Long.MAX_VALUE;\n    while (size > 0) {\n      long timeDiff = timestamp - timestamps[first];\n      if (timeDiff < 0 && (onlyOlder || -timeDiff >= previousTimeDiff)) {\n        break;\n      }\n      previousTimeDiff = timeDiff;\n      value = values[first];\n      values[first] = null;\n      first = (first + 1) % values.length;\n      size--;\n    }\n    return value;\n  }\n\n  private void clearBufferOnTimeDiscontinuity(long timestamp) {\n    if (size > 0) {\n      int last = (first + size - 1) % values.length;\n      if (timestamp <= timestamps[last]) {\n        clear();\n      }\n    }\n  }\n\n  private void doubleCapacityIfFull() {\n    int capacity = values.length;\n    if (size < capacity) {\n      return;\n    }\n    int newCapacity = capacity * 2;\n    long[] newTimestamps = new long[newCapacity];\n    V[] newValues = newArray(newCapacity);\n    // Reset the loop starting index to 0 while coping to the new buffer.\n    // First copy the values from 'first' index to the end of original array.\n    int length = capacity - first;\n    System.arraycopy(timestamps, first, newTimestamps, 0, length);\n    System.arraycopy(values, first, newValues, 0, length);\n    // Then the values from index 0 to 'first' index.\n    if (first > 0) {\n      System.arraycopy(timestamps, 0, newTimestamps, length, first);\n      System.arraycopy(values, 0, newValues, length, first);\n    }\n    timestamps = newTimestamps;\n    values = newValues;\n    first = 0;\n  }\n\n  private void addUnchecked(long timestamp, V value) {\n    int next = (first + size) % values.length;\n    timestamps[next] = timestamp;\n    values[next] = value;\n    size++;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static <V> V[] newArray(int length) {\n    return (V[]) new Object[length];\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport com.google.android.exoplayer2.C;\n\n/**\n * Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling\n * and adjustment is supported, taking into account timestamp rollover.\n */\npublic final class TimestampAdjuster {\n\n  /**\n   * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should\n   * not be offset.\n   */\n  public static final long DO_NOT_OFFSET = Long.MAX_VALUE;\n\n  /**\n   * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock\n   * presentation timestamp.\n   */\n  private static final long MAX_PTS_PLUS_ONE = 0x200000000L;\n\n  private long firstSampleTimestampUs;\n  private long timestampOffsetUs;\n\n  // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.\n  private volatile long lastSampleTimestampUs;\n\n  /**\n   * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.\n   */\n  public TimestampAdjuster(long firstSampleTimestampUs) {\n    lastSampleTimestampUs = C.TIME_UNSET;\n    setFirstSampleTimestampUs(firstSampleTimestampUs);\n  }\n\n  /**\n   * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be\n   * called before any timestamps have been adjusted.\n   *\n   * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or\n   *     {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.\n   */\n  public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {\n    Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);\n    this.firstSampleTimestampUs = firstSampleTimestampUs;\n  }\n\n  /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */\n  public long getFirstSampleTimestampUs() {\n    return firstSampleTimestampUs;\n  }\n\n  /**\n   * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link\n   * #adjustSampleTimestamp} has not been called, returns the result of calling {@link\n   * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link\n   * C#TIME_UNSET}.\n   */\n  public long getLastAdjustedTimestampUs() {\n    return lastSampleTimestampUs != C.TIME_UNSET\n        ? (lastSampleTimestampUs + timestampOffsetUs)\n        : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;\n  }\n\n  /**\n   * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.\n   * If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp\n   * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.\n   *\n   * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.\n   *     {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not\n   *     be offset.\n   */\n  public long getTimestampOffsetUs() {\n    return firstSampleTimestampUs == DO_NOT_OFFSET\n        ? 0\n        : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;\n  }\n\n  /**\n   * Resets the instance to its initial state.\n   */\n  public void reset() {\n    lastSampleTimestampUs = C.TIME_UNSET;\n  }\n\n  /**\n   * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.\n   *\n   * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.\n   * @return The adjusted timestamp in microseconds.\n   */\n  public long adjustTsTimestamp(long pts90Khz) {\n    if (pts90Khz == C.TIME_UNSET) {\n      return C.TIME_UNSET;\n    }\n    if (lastSampleTimestampUs != C.TIME_UNSET) {\n      // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),\n      // and we need to snap to the one closest to lastSampleTimestampUs.\n      long lastPts = usToPts(lastSampleTimestampUs);\n      long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;\n      long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));\n      long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);\n      pts90Khz =\n          Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)\n              ? ptsWrapBelow\n              : ptsWrapAbove;\n    }\n    return adjustSampleTimestamp(ptsToUs(pts90Khz));\n  }\n\n  /**\n   * Offsets a timestamp in microseconds.\n   *\n   * @param timeUs The timestamp to adjust in microseconds.\n   * @return The adjusted timestamp in microseconds.\n   */\n  public long adjustSampleTimestamp(long timeUs) {\n    if (timeUs == C.TIME_UNSET) {\n      return C.TIME_UNSET;\n    }\n    // Record the adjusted PTS to adjust for wraparound next time.\n    if (lastSampleTimestampUs != C.TIME_UNSET) {\n      lastSampleTimestampUs = timeUs;\n    } else {\n      if (firstSampleTimestampUs != DO_NOT_OFFSET) {\n        // Calculate the timestamp offset.\n        timestampOffsetUs = firstSampleTimestampUs - timeUs;\n      }\n      synchronized (this) {\n        lastSampleTimestampUs = timeUs;\n        // Notify threads waiting for this adjuster to be initialized.\n        notifyAll();\n      }\n    }\n    return timeUs + timestampOffsetUs;\n  }\n\n  /**\n   * Blocks the calling thread until this adjuster is initialized.\n   *\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public synchronized void waitUntilInitialized() throws InterruptedException {\n    while (lastSampleTimestampUs == C.TIME_UNSET) {\n      wait();\n    }\n  }\n\n  /**\n   * Converts a 90 kHz clock timestamp to a timestamp in microseconds.\n   *\n   * @param pts A 90 kHz clock timestamp.\n   * @return The corresponding value in microseconds.\n   */\n  public static long ptsToUs(long pts) {\n    return (pts * C.MICROS_PER_SECOND) / 90000;\n  }\n\n  /**\n   * Converts a timestamp in microseconds to a 90 kHz clock timestamp.\n   *\n   * @param us A value in microseconds.\n   * @return The corresponding value as a 90 kHz clock timestamp.\n   */\n  public static long usToPts(long us) {\n    return (us * 90000) / C.MICROS_PER_SECOND;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.annotation.TargetApi;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\n\n/**\n * Calls through to {@link android.os.Trace} methods on supported API levels.\n */\npublic final class TraceUtil {\n\n  private TraceUtil() {}\n\n  /**\n   * Writes a trace message to indicate that a given section of code has begun.\n   *\n   * @see android.os.Trace#beginSection(String)\n   * @param sectionName The name of the code section to appear in the trace. This may be at most 127\n   *     Unicode code units long.\n   */\n  public static void beginSection(String sectionName) {\n    if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {\n      beginSectionV18(sectionName);\n    }\n  }\n\n  /**\n   * Writes a trace message to indicate that a given section of code has ended.\n   *\n   * @see android.os.Trace#endSection()\n   */\n  public static void endSection() {\n    if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {\n      endSectionV18();\n    }\n  }\n\n  @TargetApi(18)\n  private static void beginSectionV18(String sectionName) {\n    android.os.Trace.beginSection(sectionName);\n  }\n\n  @TargetApi(18)\n  private static void endSectionV18() {\n    android.os.Trace.endSection();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/UriUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport androidx.annotation.Nullable;\n\n/**\n * Utility methods for manipulating URIs.\n */\npublic final class UriUtil {\n\n  /**\n   * The length of arrays returned by {@link #getUriIndices(String)}.\n   */\n  private static final int INDEX_COUNT = 4;\n  /**\n   * An index into an array returned by {@link #getUriIndices(String)}.\n   * <p>\n   * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if\n   * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),\n   * including when the URI has no scheme.\n   */\n  private static final int SCHEME_COLON = 0;\n  /**\n   * An index into an array returned by {@link #getUriIndices(String)}.\n   * <p>\n   * The value at this position in the array is the index of the path part. Equals (schemeColon + 1)\n   * if no authority part, (schemeColon + 3) if the authority part consists of just \"//\", and\n   * (query) if no path part. The characters starting at this index can be \"//\" only if the\n   * authority part is non-empty (in this case the double-slash means the first segment is empty).\n   */\n  private static final int PATH = 1;\n  /**\n   * An index into an array returned by {@link #getUriIndices(String)}.\n   * <p>\n   * The value at this position in the array is the index of the query part, including the '?'\n   * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a\n   * single '?' with no data.\n   */\n  private static final int QUERY = 2;\n  /**\n   * An index into an array returned by {@link #getUriIndices(String)}.\n   * <p>\n   * The value at this position in the array is the index of the fragment part, including the '#'\n   * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if\n   * the fragment part is a single '#' with no data.\n   */\n  private static final int FRAGMENT = 3;\n\n  private UriUtil() {}\n\n  /**\n   * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.\n   *\n   * @param baseUri The base URI.\n   * @param referenceUri The reference URI to resolve.\n   */\n  public static Uri resolveToUri(@Nullable String baseUri, @Nullable String referenceUri) {\n    return Uri.parse(resolve(baseUri, referenceUri));\n  }\n\n  /**\n   * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}.\n   *\n   * <p>The resolution is performed as specified by RFC-3986.\n   *\n   * @param baseUri The base URI.\n   * @param referenceUri The reference URI to resolve.\n   */\n  public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) {\n    StringBuilder uri = new StringBuilder();\n\n    // Map null onto empty string, to make the following logic simpler.\n    baseUri = baseUri == null ? \"\" : baseUri;\n    referenceUri = referenceUri == null ? \"\" : referenceUri;\n\n    int[] refIndices = getUriIndices(referenceUri);\n    if (refIndices[SCHEME_COLON] != -1) {\n      // The reference is absolute. The target Uri is the reference.\n      uri.append(referenceUri);\n      removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);\n      return uri.toString();\n    }\n\n    int[] baseIndices = getUriIndices(baseUri);\n    if (refIndices[FRAGMENT] == 0) {\n      // The reference is empty or contains just the fragment part, then the target Uri is the\n      // concatenation of the base Uri without its fragment, and the reference.\n      return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();\n    }\n\n    if (refIndices[QUERY] == 0) {\n      // The reference starts with the query part. The target is the base up to (but excluding) the\n      // query, plus the reference.\n      return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();\n    }\n\n    if (refIndices[PATH] != 0) {\n      // The reference has authority. The target is the base scheme plus the reference.\n      int baseLimit = baseIndices[SCHEME_COLON] + 1;\n      uri.append(baseUri, 0, baseLimit).append(referenceUri);\n      return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);\n    }\n\n    if (referenceUri.charAt(refIndices[PATH]) == '/') {\n      // The reference path is rooted. The target is the base scheme and authority (if any), plus\n      // the reference.\n      uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);\n      return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);\n    }\n\n    // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,\n    // and the reference. This can be split into 2 cases:\n    if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]\n        && baseIndices[PATH] == baseIndices[QUERY]) {\n      // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is\n      // needed after the authority, before appending the reference.\n      uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);\n      return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);\n    } else {\n      // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after\n      // it. If base hier-part has no '/', it could only mean that it is completely empty or\n      // contains only one segment, in which case the whole hier-part is excluded and the reference\n      // is appended right after the base scheme colon without an added '/'.\n      int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);\n      int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;\n      uri.append(baseUri, 0, baseLimit).append(referenceUri);\n      return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);\n    }\n  }\n\n  /**\n   * Removes query parameter from an Uri, if present.\n   *\n   * @param uri The uri.\n   * @param queryParameterName The name of the query parameter.\n   * @return The uri without the query parameter.\n   */\n  public static Uri removeQueryParameter(Uri uri, String queryParameterName) {\n    Uri.Builder builder = uri.buildUpon();\n    builder.clearQuery();\n    for (String key : uri.getQueryParameterNames()) {\n      if (!key.equals(queryParameterName)) {\n        for (String value : uri.getQueryParameters(key)) {\n          builder.appendQueryParameter(key, value);\n        }\n      }\n    }\n    return builder.build();\n  }\n\n  /**\n   * Removes dot segments from the path of a URI.\n   *\n   * @param uri A {@link StringBuilder} containing the URI.\n   * @param offset The index of the start of the path in {@code uri}.\n   * @param limit The limit (exclusive) of the path in {@code uri}.\n   */\n  private static String removeDotSegments(StringBuilder uri, int offset, int limit) {\n    if (offset >= limit) {\n      // Nothing to do.\n      return uri.toString();\n    }\n    if (uri.charAt(offset) == '/') {\n      // If the path starts with a /, always retain it.\n      offset++;\n    }\n    // The first character of the current path segment.\n    int segmentStart = offset;\n    int i = offset;\n    while (i <= limit) {\n      int nextSegmentStart;\n      if (i == limit) {\n        nextSegmentStart = i;\n      } else if (uri.charAt(i) == '/') {\n        nextSegmentStart = i + 1;\n      } else {\n        i++;\n        continue;\n      }\n      // We've encountered the end of a segment or the end of the path. If the final segment was\n      // \".\" or \"..\", remove the appropriate segments of the path.\n      if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {\n        // Given \"abc/def/./ghi\", remove \"./\" to get \"abc/def/ghi\".\n        uri.delete(segmentStart, nextSegmentStart);\n        limit -= nextSegmentStart - segmentStart;\n        i = segmentStart;\n      } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.'\n          && uri.charAt(segmentStart + 1) == '.') {\n        // Given \"abc/def/../ghi\", remove \"def/../\" to get \"abc/ghi\".\n        int prevSegmentStart = uri.lastIndexOf(\"/\", segmentStart - 2) + 1;\n        int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;\n        uri.delete(removeFrom, nextSegmentStart);\n        limit -= nextSegmentStart - removeFrom;\n        segmentStart = prevSegmentStart;\n        i = prevSegmentStart;\n      } else {\n        i++;\n        segmentStart = i;\n      }\n    }\n    return uri.toString();\n  }\n\n  /**\n   * Calculates indices of the constituent components of a URI.\n   *\n   * @param uriString The URI as a string.\n   * @return The corresponding indices.\n   */\n  private static int[] getUriIndices(String uriString) {\n    int[] indices = new int[INDEX_COUNT];\n    if (TextUtils.isEmpty(uriString)) {\n      indices[SCHEME_COLON] = -1;\n      return indices;\n    }\n\n    // Determine outer structure from right to left.\n    // Uri = scheme \":\" hier-part [ \"?\" query ] [ \"#\" fragment ]\n    int length = uriString.length();\n    int fragmentIndex = uriString.indexOf('#');\n    if (fragmentIndex == -1) {\n      fragmentIndex = length;\n    }\n    int queryIndex = uriString.indexOf('?');\n    if (queryIndex == -1 || queryIndex > fragmentIndex) {\n      // '#' before '?': '?' is within the fragment.\n      queryIndex = fragmentIndex;\n    }\n    // Slashes are allowed only in hier-part so any colon after the first slash is part of the\n    // hier-part, not the scheme colon separator.\n    int schemeIndexLimit = uriString.indexOf('/');\n    if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {\n      schemeIndexLimit = queryIndex;\n    }\n    int schemeIndex = uriString.indexOf(':');\n    if (schemeIndex > schemeIndexLimit) {\n      // '/' before ':'\n      schemeIndex = -1;\n    }\n\n    // Determine hier-part structure: hier-part = \"//\" authority path / path\n    // This block can also cope with schemeIndex == -1.\n    boolean hasAuthority = schemeIndex + 2 < queryIndex\n        && uriString.charAt(schemeIndex + 1) == '/'\n        && uriString.charAt(schemeIndex + 2) == '/';\n    int pathIndex;\n    if (hasAuthority) {\n      pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after \"://\"\n      if (pathIndex == -1 || pathIndex > queryIndex) {\n        pathIndex = queryIndex;\n      }\n    } else {\n      pathIndex = schemeIndex + 1;\n    }\n\n    indices[SCHEME_COLON] = schemeIndex;\n    indices[PATH] = pathIndex;\n    indices[QUERY] = queryIndex;\n    indices[FRAGMENT] = fragmentIndex;\n    return indices;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/Util.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport static android.content.Context.UI_MODE_SERVICE;\n\nimport android.Manifest.permission;\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.app.UiModeManager;\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.content.pm.PackageManager.NameNotFoundException;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.graphics.Point;\nimport android.media.AudioFormat;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Parcel;\nimport android.security.NetworkSecurityPolicy;\nimport android.telephony.TelephonyManager;\nimport android.text.TextUtils;\nimport android.view.Display;\nimport android.view.WindowManager;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.lang.reflect.Method;\nimport java.math.BigDecimal;\nimport java.nio.charset.Charset;\nimport java.util.Arrays;\nimport java.util.Calendar;\nimport java.util.Collections;\nimport java.util.Formatter;\nimport java.util.GregorianCalendar;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.MissingResourceException;\nimport java.util.TimeZone;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.DataFormatException;\nimport java.util.zip.Inflater;\nimport org.checkerframework.checker.initialization.qual.UnknownInitialization;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\nimport org.checkerframework.checker.nullness.qual.PolyNull;\n\n/**\n * Miscellaneous utility methods.\n */\npublic final class Util {\n\n  /**\n   * Like {@link Build.VERSION#SDK_INT}, but in a place where it can be conveniently\n   * overridden for local testing.\n   */\n  public static final int SDK_INT = Build.VERSION.SDK_INT;\n\n  /**\n   * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local\n   * testing.\n   */\n  public static final String DEVICE = Build.DEVICE;\n\n  /**\n   * Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for\n   * local testing.\n   */\n  public static final String MANUFACTURER = Build.MANUFACTURER;\n\n  /**\n   * Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local\n   * testing.\n   */\n  public static final String MODEL = Build.MODEL;\n\n  /**\n   * A concise description of the device that it can be useful to log for debugging purposes.\n   */\n  public static final String DEVICE_DEBUG_INFO = DEVICE + \", \" + MODEL + \", \" + MANUFACTURER + \", \"\n      + SDK_INT;\n\n  /** An empty byte array. */\n  public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];\n\n  private static final String TAG = \"Util\";\n  private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(\n      \"(\\\\d\\\\d\\\\d\\\\d)\\\\-(\\\\d\\\\d)\\\\-(\\\\d\\\\d)[Tt]\"\n      + \"(\\\\d\\\\d):(\\\\d\\\\d):(\\\\d\\\\d)([\\\\.,](\\\\d+))?\"\n      + \"([Zz]|((\\\\+|\\\\-)(\\\\d?\\\\d):?(\\\\d\\\\d)))?\");\n  private static final Pattern XS_DURATION_PATTERN =\n      Pattern.compile(\"^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?\"\n          + \"(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$\");\n  private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile(\"%([A-Fa-f0-9]{2})\");\n\n  // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter\n  // ISO 639-2 code back to the corresponding 2-letter code.\n  @Nullable private static HashMap<String, String> languageTagIso3ToIso2;\n\n  private Util() {}\n\n  /**\n   * Converts the entirety of an {@link InputStream} to a byte array.\n   *\n   * @param inputStream the {@link InputStream} to be read. The input stream is not closed by this\n   *     method.\n   * @return a byte array containing all of the inputStream's bytes.\n   * @throws IOException if an error occurs reading from the stream.\n   */\n  public static byte[] toByteArray(InputStream inputStream) throws IOException {\n    byte[] buffer = new byte[1024 * 4];\n    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();\n    int bytesRead;\n    while ((bytesRead = inputStream.read(buffer)) != -1) {\n      outputStream.write(buffer, 0, bytesRead);\n    }\n    return outputStream.toByteArray();\n  }\n\n  /**\n   * Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or\n   * {@link Context#startService(Intent)} otherwise.\n   *\n   * @param context The context to call.\n   * @param intent The intent to pass to the called method.\n   * @return The result of the called method.\n   */\n  @Nullable\n  public static ComponentName startForegroundService(Context context, Intent intent) {\n    if (Util.SDK_INT >= 26) {\n      return context.startForegroundService(intent);\n    } else {\n      return context.startService(intent);\n    }\n  }\n\n  /**\n   * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}\n   * permission read the specified {@link Uri}s, requesting the permission if necessary.\n   *\n   * @param activity The host activity for checking and requesting the permission.\n   * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read.\n   * @return Whether a permission request was made.\n   */\n  @TargetApi(23)\n  public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {\n    if (Util.SDK_INT < 23) {\n      return false;\n    }\n    for (Uri uri : uris) {\n      if (isLocalFileUri(uri)) {\n        if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE)\n            != PackageManager.PERMISSION_GRANTED) {\n          activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);\n          return true;\n        }\n        break;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Returns whether it may be possible to load the given URIs based on the network security\n   * policy's cleartext traffic permissions.\n   *\n   * @param uris A list of URIs that will be loaded.\n   * @return Whether it may be possible to load the given URIs.\n   */\n  @TargetApi(24)\n  public static boolean checkCleartextTrafficPermitted(Uri... uris) {\n    if (Util.SDK_INT < 24) {\n      // We assume cleartext traffic is permitted.\n      return true;\n    }\n    for (Uri uri : uris) {\n      if (\"http\".equals(uri.getScheme())\n          && !NetworkSecurityPolicy.getInstance()\n              .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost()))) {\n        // The security policy prevents cleartext traffic.\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Returns true if the URI is a path to a local file or a reference to a local file.\n   *\n   * @param uri The uri to test.\n   */\n  public static boolean isLocalFileUri(Uri uri) {\n    String scheme = uri.getScheme();\n    return TextUtils.isEmpty(scheme) || \"file\".equals(scheme);\n  }\n\n  /**\n   * Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or\n   * both may be null.\n   *\n   * @param o1 The first object.\n   * @param o2 The second object.\n   * @return {@code o1 == null ? o2 == null : o1.equals(o2)}.\n   */\n  public static boolean areEqual(@Nullable Object o1, @Nullable Object o2) {\n    return o1 == null ? o2 == null : o1.equals(o2);\n  }\n\n  /**\n   * Tests whether an {@code items} array contains an object equal to {@code item}, according to\n   * {@link Object#equals(Object)}.\n   *\n   * <p>If {@code item} is null then true is returned if and only if {@code items} contains null.\n   *\n   * @param items The array of items to search.\n   * @param item The item to search for.\n   * @return True if the array contains an object equal to the item being searched for.\n   */\n  public static boolean contains(@NullableType Object[] items, @Nullable Object item) {\n    for (Object arrayItem : items) {\n      if (areEqual(arrayItem, item)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Removes an indexed range from a List.\n   *\n   * <p>Does nothing if the provided range is valid and {@code fromIndex == toIndex}.\n   *\n   * @param list The List to remove the range from.\n   * @param fromIndex The first index to be removed (inclusive).\n   * @param toIndex The last index to be removed (exclusive).\n   * @throws IllegalArgumentException If {@code fromIndex} &lt; 0, {@code toIndex} &gt; {@code\n   *     list.size()}, or {@code fromIndex} &gt; {@code toIndex}.\n   */\n  public static <T> void removeRange(List<T> list, int fromIndex, int toIndex) {\n    if (fromIndex < 0 || toIndex > list.size() || fromIndex > toIndex) {\n      throw new IllegalArgumentException();\n    } else if (fromIndex != toIndex) {\n      // Checking index inequality prevents an unnecessary allocation.\n      list.subList(fromIndex, toIndex).clear();\n    }\n  }\n\n  /**\n   * Casts a nullable variable to a non-null variable without runtime null check.\n   *\n   * <p>Use {@link Assertions#checkNotNull(Object)} to throw if the value is null.\n   */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull(\"#1\")\n  public static <T> T castNonNull(@Nullable T value) {\n    return value;\n  }\n\n  /** Casts a nullable type array to a non-null type array without runtime null check. */\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\", \"return.type.incompatible\"})\n  @EnsuresNonNull(\"#1\")\n  public static <T> T[] castNonNullTypeArray(@NullableType T[] value) {\n    return value;\n  }\n\n  /**\n   * Copies and optionally truncates an array. Prevents null array elements created by {@link\n   * Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length.\n   *\n   * @param input The input array.\n   * @param length The output array length. Must be less or equal to the length of the input array.\n   * @return The copied array.\n   */\n  @SuppressWarnings({\"nullness:argument.type.incompatible\", \"nullness:return.type.incompatible\"})\n  public static <T> T[] nullSafeArrayCopy(T[] input, int length) {\n    Assertions.checkArgument(length <= input.length);\n    return Arrays.copyOf(input, length);\n  }\n\n  /**\n   * Copies a subset of an array.\n   *\n   * @param input The input array.\n   * @param from The start the range to be copied, inclusive\n   * @param to The end of the range to be copied, exclusive.\n   * @return The copied array.\n   */\n  @SuppressWarnings({\"nullness:argument.type.incompatible\", \"nullness:return.type.incompatible\"})\n  public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) {\n    Assertions.checkArgument(0 <= from);\n    Assertions.checkArgument(to <= input.length);\n    return Arrays.copyOfRange(input, from, to);\n  }\n\n  /**\n   * Creates a new array containing {@code original} with {@code newElement} appended.\n   *\n   * @param original The input array.\n   * @param newElement The element to append.\n   * @return The new array.\n   */\n  public static <T> T[] nullSafeArrayAppend(T[] original, T newElement) {\n    @NullableType T[] result = Arrays.copyOf(original, original.length + 1);\n    result[original.length] = newElement;\n    return castNonNullTypeArray(result);\n  }\n\n  /**\n   * Creates a new array containing the concatenation of two non-null type arrays.\n   *\n   * @param first The first array.\n   * @param second The second array.\n   * @return The concatenated result.\n   */\n  @SuppressWarnings({\"nullness:assignment.type.incompatible\"})\n  public static <T> T[] nullSafeArrayConcatenation(T[] first, T[] second) {\n    T[] concatenation = Arrays.copyOf(first, first.length + second.length);\n    System.arraycopy(\n        /* src= */ second,\n        /* srcPos= */ 0,\n        /* dest= */ concatenation,\n        /* destPos= */ first.length,\n        /* length= */ second.length);\n    return concatenation;\n  }\n  /**\n   * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link\n   * Looper} thread. The method accepts partially initialized objects as callback under the\n   * assumption that the Handler won't be used to send messages until the callback is fully\n   * initialized.\n   *\n   * <p>If the current thread doesn't have a {@link Looper}, the application's main thread {@link\n   * Looper} is used.\n   *\n   * @param callback A {@link Handler.Callback}. May be a partially initialized class.\n   * @return A {@link Handler} with the specified callback on the current {@link Looper} thread.\n   */\n  public static Handler createHandler(Handler.@UnknownInitialization Callback callback) {\n    return createHandler(getLooper(), callback);\n  }\n\n  /**\n   * Creates a {@link Handler} with the specified {@link Handler.Callback} on the specified {@link\n   * Looper} thread. The method accepts partially initialized objects as callback under the\n   * assumption that the Handler won't be used to send messages until the callback is fully\n   * initialized.\n   *\n   * @param looper A {@link Looper} to run the callback on.\n   * @param callback A {@link Handler.Callback}. May be a partially initialized class.\n   * @return A {@link Handler} with the specified callback on the current {@link Looper} thread.\n   */\n  @SuppressWarnings({\"nullness:argument.type.incompatible\", \"nullness:return.type.incompatible\"})\n  public static Handler createHandler(\n      Looper looper, Handler.@UnknownInitialization Callback callback) {\n    return new Handler(looper, callback);\n  }\n\n  /**\n   * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the\n   * application's main thread if the current thread doesn't have a {@link Looper}.\n   */\n  public static Looper getLooper() {\n    Looper myLooper = Looper.myLooper();\n    return myLooper != null ? myLooper : Looper.getMainLooper();\n  }\n\n  /**\n   * Instantiates a new single threaded executor whose thread has the specified name.\n   *\n   * @param threadName The name of the thread.\n   * @return The executor.\n   */\n  public static ExecutorService newSingleThreadExecutor(final String threadName) {\n    return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));\n  }\n\n  /**\n   * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.\n   *\n   * @param dataSource The {@link DataSource} to close.\n   */\n  public static void closeQuietly(@Nullable DataSource dataSource) {\n    try {\n      if (dataSource != null) {\n        dataSource.close();\n      }\n    } catch (IOException e) {\n      // Ignore.\n    }\n  }\n\n  /**\n   * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link\n   * java.io.OutputStream} and {@link InputStream} are {@code Closeable}.\n   *\n   * @param closeable The {@link Closeable} to close.\n   */\n  public static void closeQuietly(@Nullable Closeable closeable) {\n    try {\n      if (closeable != null) {\n        closeable.close();\n      }\n    } catch (IOException e) {\n      // Ignore.\n    }\n  }\n\n  /**\n   * Reads an integer from a {@link Parcel} and interprets it as a boolean, with 0 mapping to false\n   * and all other values mapping to true.\n   *\n   * @param parcel The {@link Parcel} to read from.\n   * @return The read value.\n   */\n  public static boolean readBoolean(Parcel parcel) {\n    return parcel.readInt() != 0;\n  }\n\n  /**\n   * Writes a boolean to a {@link Parcel}. The boolean is written as an integer with value 1 (true)\n   * or 0 (false).\n   *\n   * @param parcel The {@link Parcel} to write to.\n   * @param value The value to write.\n   */\n  public static void writeBoolean(Parcel parcel, boolean value) {\n    parcel.writeInt(value ? 1 : 0);\n  }\n\n  /**\n   * Returns the language tag for a {@link Locale}.\n   *\n   * <p>For API levels &ge; 21, this tag is IETF BCP 47 compliant. Use {@link\n   * #normalizeLanguageCode(String)} to retrieve a normalized IETF BCP 47 language tag for all API\n   * levels if needed.\n   *\n   * @param locale A {@link Locale}.\n   * @return The language tag.\n   */\n  public static String getLocaleLanguageTag(Locale locale) {\n    return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();\n  }\n\n  /**\n   * Returns a normalized IETF BCP 47 language tag for {@code language}.\n   *\n   * @param language A case-insensitive language code supported by {@link\n   *     Locale#forLanguageTag(String)}.\n   * @return The all-lowercase normalized code, or null if the input was null, or {@code\n   *     language.toLowerCase()} if the language could not be normalized.\n   */\n  public static @PolyNull String normalizeLanguageCode(@PolyNull String language) {\n    if (language == null) {\n      return null;\n    }\n    // Locale data (especially for API < 21) may produce tags with '_' instead of the\n    // standard-conformant '-'.\n    String normalizedTag = language.replace('_', '-');\n    if (Util.SDK_INT >= 21) {\n      // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags.\n      normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag);\n    }\n    if (normalizedTag.isEmpty() || \"und\".equals(normalizedTag)) {\n      // Tag isn't valid, keep using the original.\n      normalizedTag = language;\n    }\n    normalizedTag = Util.toLowerInvariant(normalizedTag);\n    String mainLanguage = Util.splitAtFirst(normalizedTag, \"-\")[0];\n    if (mainLanguage.length() == 3) {\n      // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO\n      // 639-1 codes automatically.\n      if (languageTagIso3ToIso2 == null) {\n        languageTagIso3ToIso2 = createIso3ToIso2Map();\n      }\n      String iso2Language = languageTagIso3ToIso2.get(mainLanguage);\n      if (iso2Language != null) {\n        normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3);\n      }\n    }\n    return normalizedTag;\n  }\n\n  /**\n   * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes.\n   *\n   * @param bytes The UTF-8 encoded bytes to decode.\n   * @return The string.\n   */\n  public static String fromUtf8Bytes(byte[] bytes) {\n    return new String(bytes, Charset.forName(C.UTF8_NAME));\n  }\n\n  /**\n   * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray.\n   *\n   * @param bytes The UTF-8 encoded bytes to decode.\n   * @param offset The index of the first byte to decode.\n   * @param length The number of bytes to decode.\n   * @return The string.\n   */\n  public static String fromUtf8Bytes(byte[] bytes, int offset, int length) {\n    return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME));\n  }\n\n  /**\n   * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.\n   *\n   * @param value The {@link String} whose bytes should be obtained.\n   * @return The code points encoding using UTF-8.\n   */\n  public static byte[] getUtf8Bytes(String value) {\n    return value.getBytes(Charset.forName(C.UTF8_NAME));\n  }\n\n  /**\n   * Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link\n   * String#split(String)} but empty matches at the end of the string will not be omitted from the\n   * returned array.\n   *\n   * @param value The string to split.\n   * @param regex A delimiting regular expression.\n   * @return The array of strings resulting from splitting the string.\n   */\n  public static String[] split(String value, String regex) {\n    return value.split(regex, /* limit= */ -1);\n  }\n\n  /**\n   * Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does\n   * not match, returns an array with one element which is the input string. If the delimiter does\n   * match, returns an array with the portion of the string before the delimiter and the rest of the\n   * string.\n   *\n   * @param value The string.\n   * @param regex A delimiting regular expression.\n   * @return The string split by the first occurrence of the delimiter.\n   */\n  public static String[] splitAtFirst(String value, String regex) {\n    return value.split(regex, /* limit= */ 2);\n  }\n\n  /**\n   * Returns whether the given character is a carriage return ('\\r') or a line feed ('\\n').\n   *\n   * @param c The character.\n   * @return Whether the given character is a linebreak.\n   */\n  public static boolean isLinebreak(int c) {\n    return c == '\\n' || c == '\\r';\n  }\n\n  /**\n   * Converts text to lower case using {@link Locale#US}.\n   *\n   * @param text The text to convert.\n   * @return The lower case text, or null if {@code text} is null.\n   */\n  public static @PolyNull String toLowerInvariant(@PolyNull String text) {\n    return text == null ? text : text.toLowerCase(Locale.US);\n  }\n\n  /**\n   * Converts text to upper case using {@link Locale#US}.\n   *\n   * @param text The text to convert.\n   * @return The upper case text, or null if {@code text} is null.\n   */\n  public static @PolyNull String toUpperInvariant(@PolyNull String text) {\n    return text == null ? text : text.toUpperCase(Locale.US);\n  }\n\n  /**\n   * Formats a string using {@link Locale#US}.\n   *\n   * @see String#format(String, Object...)\n   */\n  public static String formatInvariant(String format, Object... args) {\n    return String.format(Locale.US, format, args);\n  }\n\n  /**\n   * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.\n   *\n   * @param numerator The numerator to divide.\n   * @param denominator The denominator to divide by.\n   * @return The ceiled result of the division.\n   */\n  public static int ceilDivide(int numerator, int denominator) {\n    return (numerator + denominator - 1) / denominator;\n  }\n\n  /**\n   * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.\n   *\n   * @param numerator The numerator to divide.\n   * @param denominator The denominator to divide by.\n   * @return The ceiled result of the division.\n   */\n  public static long ceilDivide(long numerator, long denominator) {\n    return (numerator + denominator - 1) / denominator;\n  }\n\n  /**\n   * Constrains a value to the specified bounds.\n   *\n   * @param value The value to constrain.\n   * @param min The lower bound.\n   * @param max The upper bound.\n   * @return The constrained value {@code Math.max(min, Math.min(value, max))}.\n   */\n  public static int constrainValue(int value, int min, int max) {\n    return Math.max(min, Math.min(value, max));\n  }\n\n  /**\n   * Constrains a value to the specified bounds.\n   *\n   * @param value The value to constrain.\n   * @param min The lower bound.\n   * @param max The upper bound.\n   * @return The constrained value {@code Math.max(min, Math.min(value, max))}.\n   */\n  public static long constrainValue(long value, long min, long max) {\n    return Math.max(min, Math.min(value, max));\n  }\n\n  /**\n   * Constrains a value to the specified bounds.\n   *\n   * @param value The value to constrain.\n   * @param min The lower bound.\n   * @param max The upper bound.\n   * @return The constrained value {@code Math.max(min, Math.min(value, max))}.\n   */\n  public static float constrainValue(float value, float min, float max) {\n    return Math.max(min, Math.min(value, max));\n  }\n\n  /**\n   * Returns the sum of two arguments, or a third argument if the result overflows.\n   *\n   * @param x The first value.\n   * @param y The second value.\n   * @param overflowResult The return value if {@code x + y} overflows.\n   * @return {@code x + y}, or {@code overflowResult} if the result overflows.\n   */\n  public static long addWithOverflowDefault(long x, long y, long overflowResult) {\n    long result = x + y;\n    // See Hacker's Delight 2-13 (H. Warren Jr).\n    if (((x ^ result) & (y ^ result)) < 0) {\n      return overflowResult;\n    }\n    return result;\n  }\n\n  /**\n   * Returns the difference between two arguments, or a third argument if the result overflows.\n   *\n   * @param x The first value.\n   * @param y The second value.\n   * @param overflowResult The return value if {@code x - y} overflows.\n   * @return {@code x - y}, or {@code overflowResult} if the result overflows.\n   */\n  public static long subtractWithOverflowDefault(long x, long y, long overflowResult) {\n    long result = x - y;\n    // See Hacker's Delight 2-13 (H. Warren Jr).\n    if (((x ^ y) & (x ^ result)) < 0) {\n      return overflowResult;\n    }\n    return result;\n  }\n\n  /**\n   * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link\n   * C#INDEX_UNSET} if {@code value} is not contained in {@code array}.\n   *\n   * @param array The array to search.\n   * @param value The value to search for.\n   * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET}\n   *     if {@code value} is not contained in {@code array}.\n   */\n  public static int linearSearch(int[] array, int value) {\n    for (int i = 0; i < array.length; i++) {\n      if (array[i] == value) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  /**\n   * Returns the index of the largest element in {@code array} that is less than (or optionally\n   * equal to) a specified {@code value}.\n   *\n   * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the\n   * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the\n   * index of the first one will be returned.\n   *\n   * @param array The array to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the array, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the largest element strictly less\n   *     than the value.\n   * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than\n   *     the smallest element in the array. If false then -1 will be returned.\n   * @return The index of the largest element in {@code array} that is less than (or optionally\n   *     equal to) {@code value}.\n   */\n  public static int binarySearchFloor(\n      int[] array, int value, boolean inclusive, boolean stayInBounds) {\n    int index = Arrays.binarySearch(array, value);\n    if (index < 0) {\n      index = -(index + 2);\n    } else {\n      while (--index >= 0 && array[index] == value) {}\n      if (inclusive) {\n        index++;\n      }\n    }\n    return stayInBounds ? Math.max(0, index) : index;\n  }\n\n  /**\n   * Returns the index of the largest element in {@code array} that is less than (or optionally\n   * equal to) a specified {@code value}.\n   * <p>\n   * The search is performed using a binary search algorithm, so the array must be sorted. If the\n   * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the\n   * index of the first one will be returned.\n   *\n   * @param array The array to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the array, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the largest element strictly less\n   *     than the value.\n   * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than\n   *     the smallest element in the array. If false then -1 will be returned.\n   * @return The index of the largest element in {@code array} that is less than (or optionally\n   *     equal to) {@code value}.\n   */\n  public static int binarySearchFloor(long[] array, long value, boolean inclusive,\n      boolean stayInBounds) {\n    int index = Arrays.binarySearch(array, value);\n    if (index < 0) {\n      index = -(index + 2);\n    } else {\n      while (--index >= 0 && array[index] == value) {}\n      if (inclusive) {\n        index++;\n      }\n    }\n    return stayInBounds ? Math.max(0, index) : index;\n  }\n\n  /**\n   * Returns the index of the largest element in {@code list} that is less than (or optionally equal\n   * to) a specified {@code value}.\n   *\n   * <p>The search is performed using a binary search algorithm, so the list must be sorted. If the\n   * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index\n   * of the first one will be returned.\n   *\n   * @param <T> The type of values being searched.\n   * @param list The list to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the list, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the largest element strictly less\n   *     than the value.\n   * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than\n   *     the smallest element in the list. If false then -1 will be returned.\n   * @return The index of the largest element in {@code list} that is less than (or optionally equal\n   *     to) {@code value}.\n   */\n  public static <T extends Comparable<? super T>> int binarySearchFloor(\n      List<? extends Comparable<? super T>> list,\n      T value,\n      boolean inclusive,\n      boolean stayInBounds) {\n    int index = Collections.binarySearch(list, value);\n    if (index < 0) {\n      index = -(index + 2);\n    } else {\n      while (--index >= 0 && list.get(index).compareTo(value) == 0) {}\n      if (inclusive) {\n        index++;\n      }\n    }\n    return stayInBounds ? Math.max(0, index) : index;\n  }\n\n  /**\n   * Returns the index of the smallest element in {@code array} that is greater than (or optionally\n   * equal to) a specified {@code value}.\n   *\n   * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the\n   * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the\n   * index of the last one will be returned.\n   *\n   * @param array The array to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the array, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the smallest element strictly\n   *     greater than the value.\n   * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the\n   *     value is greater than the largest element in the array. If false then {@code a.length} will\n   *     be returned.\n   * @return The index of the smallest element in {@code array} that is greater than (or optionally\n   *     equal to) {@code value}.\n   */\n  public static int binarySearchCeil(\n      int[] array, int value, boolean inclusive, boolean stayInBounds) {\n    int index = Arrays.binarySearch(array, value);\n    if (index < 0) {\n      index = ~index;\n    } else {\n      while (++index < array.length && array[index] == value) {}\n      if (inclusive) {\n        index--;\n      }\n    }\n    return stayInBounds ? Math.min(array.length - 1, index) : index;\n  }\n\n  /**\n   * Returns the index of the smallest element in {@code array} that is greater than (or optionally\n   * equal to) a specified {@code value}.\n   *\n   * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the\n   * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the\n   * index of the last one will be returned.\n   *\n   * @param array The array to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the array, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the smallest element strictly\n   *     greater than the value.\n   * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the\n   *     value is greater than the largest element in the array. If false then {@code a.length} will\n   *     be returned.\n   * @return The index of the smallest element in {@code array} that is greater than (or optionally\n   *     equal to) {@code value}.\n   */\n  public static int binarySearchCeil(\n      long[] array, long value, boolean inclusive, boolean stayInBounds) {\n    int index = Arrays.binarySearch(array, value);\n    if (index < 0) {\n      index = ~index;\n    } else {\n      while (++index < array.length && array[index] == value) {}\n      if (inclusive) {\n        index--;\n      }\n    }\n    return stayInBounds ? Math.min(array.length - 1, index) : index;\n  }\n\n  /**\n   * Returns the index of the smallest element in {@code list} that is greater than (or optionally\n   * equal to) a specified value.\n   *\n   * <p>The search is performed using a binary search algorithm, so the list must be sorted. If the\n   * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index\n   * of the last one will be returned.\n   *\n   * @param <T> The type of values being searched.\n   * @param list The list to search.\n   * @param value The value being searched for.\n   * @param inclusive If the value is present in the list, whether to return the corresponding\n   *     index. If false then the returned index corresponds to the smallest element strictly\n   *     greater than the value.\n   * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that\n   *     the value is greater than the largest element in the list. If false then {@code\n   *     list.size()} will be returned.\n   * @return The index of the smallest element in {@code list} that is greater than (or optionally\n   *     equal to) {@code value}.\n   */\n  public static <T extends Comparable<? super T>> int binarySearchCeil(\n      List<? extends Comparable<? super T>> list,\n      T value,\n      boolean inclusive,\n      boolean stayInBounds) {\n    int index = Collections.binarySearch(list, value);\n    if (index < 0) {\n      index = ~index;\n    } else {\n      int listSize = list.size();\n      while (++index < listSize && list.get(index).compareTo(value) == 0) {}\n      if (inclusive) {\n        index--;\n      }\n    }\n    return stayInBounds ? Math.min(list.size() - 1, index) : index;\n  }\n\n  /**\n   * Compares two long values and returns the same value as {@code Long.compare(long, long)}.\n   *\n   * @param left The left operand.\n   * @param right The right operand.\n   * @return 0, if left == right, a negative value if left &lt; right, or a positive value if left\n   *     &gt; right.\n   */\n  public static int compareLong(long left, long right) {\n    return left < right ? -1 : left == right ? 0 : 1;\n  }\n\n  /**\n   * Parses an xs:duration attribute value, returning the parsed duration in milliseconds.\n   *\n   * @param value The attribute value to decode.\n   * @return The parsed duration in milliseconds.\n   */\n  public static long parseXsDuration(String value) {\n    Matcher matcher = XS_DURATION_PATTERN.matcher(value);\n    if (matcher.matches()) {\n      boolean negated = !TextUtils.isEmpty(matcher.group(1));\n      // Durations containing years and months aren't completely defined. We assume there are\n      // 30.4368 days in a month, and 365.242 days in a year.\n      String years = matcher.group(3);\n      double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;\n      String months = matcher.group(5);\n      durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;\n      String days = matcher.group(7);\n      durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;\n      String hours = matcher.group(10);\n      durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;\n      String minutes = matcher.group(12);\n      durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;\n      String seconds = matcher.group(14);\n      durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;\n      long durationMillis = (long) (durationSeconds * 1000);\n      return negated ? -durationMillis : durationMillis;\n    } else {\n      return (long) (Double.parseDouble(value) * 3600 * 1000);\n    }\n  }\n\n  /**\n   * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since\n   * the epoch.\n   *\n   * @param value The attribute value to decode.\n   * @return The parsed timestamp in milliseconds since the epoch.\n   * @throws ParserException if an error occurs parsing the dateTime attribute value.\n   */\n  public static long parseXsDateTime(String value) throws ParserException {\n    Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);\n    if (!matcher.matches()) {\n      throw new ParserException(\"Invalid date/time format: \" + value);\n    }\n\n    int timezoneShift;\n    if (matcher.group(9) == null) {\n      // No time zone specified.\n      timezoneShift = 0;\n    } else if (matcher.group(9).equalsIgnoreCase(\"Z\")) {\n      timezoneShift = 0;\n    } else {\n      timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60\n          + Integer.parseInt(matcher.group(13))));\n      if (\"-\".equals(matcher.group(11))) {\n        timezoneShift *= -1;\n      }\n    }\n\n    Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone(\"GMT\"));\n\n    dateTime.clear();\n    // Note: The month value is 0-based, hence the -1 on group(2)\n    dateTime.set(Integer.parseInt(matcher.group(1)),\n                 Integer.parseInt(matcher.group(2)) - 1,\n                 Integer.parseInt(matcher.group(3)),\n                 Integer.parseInt(matcher.group(4)),\n                 Integer.parseInt(matcher.group(5)),\n                 Integer.parseInt(matcher.group(6)));\n    if (!TextUtils.isEmpty(matcher.group(8))) {\n      final BigDecimal bd = new BigDecimal(\"0.\" + matcher.group(8));\n      // we care only for milliseconds, so movePointRight(3)\n      dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());\n    }\n\n    long time = dateTime.getTimeInMillis();\n    if (timezoneShift != 0) {\n      time -= timezoneShift * 60000;\n    }\n\n    return time;\n  }\n\n  /**\n   * Scales a large timestamp.\n   * <p>\n   * Logically, scaling consists of a multiplication followed by a division. The actual operations\n   * performed are designed to minimize the probability of overflow.\n   *\n   * @param timestamp The timestamp to scale.\n   * @param multiplier The multiplier.\n   * @param divisor The divisor.\n   * @return The scaled timestamp.\n   */\n  public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {\n    if (divisor >= multiplier && (divisor % multiplier) == 0) {\n      long divisionFactor = divisor / multiplier;\n      return timestamp / divisionFactor;\n    } else if (divisor < multiplier && (multiplier % divisor) == 0) {\n      long multiplicationFactor = multiplier / divisor;\n      return timestamp * multiplicationFactor;\n    } else {\n      double multiplicationFactor = (double) multiplier / divisor;\n      return (long) (timestamp * multiplicationFactor);\n    }\n  }\n\n  /**\n   * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.\n   *\n   * @param timestamps The timestamps to scale.\n   * @param multiplier The multiplier.\n   * @param divisor The divisor.\n   * @return The scaled timestamps.\n   */\n  public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {\n    long[] scaledTimestamps = new long[timestamps.size()];\n    if (divisor >= multiplier && (divisor % multiplier) == 0) {\n      long divisionFactor = divisor / multiplier;\n      for (int i = 0; i < scaledTimestamps.length; i++) {\n        scaledTimestamps[i] = timestamps.get(i) / divisionFactor;\n      }\n    } else if (divisor < multiplier && (multiplier % divisor) == 0) {\n      long multiplicationFactor = multiplier / divisor;\n      for (int i = 0; i < scaledTimestamps.length; i++) {\n        scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;\n      }\n    } else {\n      double multiplicationFactor = (double) multiplier / divisor;\n      for (int i = 0; i < scaledTimestamps.length; i++) {\n        scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);\n      }\n    }\n    return scaledTimestamps;\n  }\n\n  /**\n   * Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.\n   *\n   * @param timestamps The timestamps to scale.\n   * @param multiplier The multiplier.\n   * @param divisor The divisor.\n   */\n  public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {\n    if (divisor >= multiplier && (divisor % multiplier) == 0) {\n      long divisionFactor = divisor / multiplier;\n      for (int i = 0; i < timestamps.length; i++) {\n        timestamps[i] /= divisionFactor;\n      }\n    } else if (divisor < multiplier && (multiplier % divisor) == 0) {\n      long multiplicationFactor = multiplier / divisor;\n      for (int i = 0; i < timestamps.length; i++) {\n        timestamps[i] *= multiplicationFactor;\n      }\n    } else {\n      double multiplicationFactor = (double) multiplier / divisor;\n      for (int i = 0; i < timestamps.length; i++) {\n        timestamps[i] = (long) (timestamps[i] * multiplicationFactor);\n      }\n    }\n  }\n\n  /**\n   * Returns the duration of media that will elapse in {@code playoutDuration}.\n   *\n   * @param playoutDuration The duration to scale.\n   * @param speed The playback speed.\n   * @return The scaled duration, in the same units as {@code playoutDuration}.\n   */\n  public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) {\n    if (speed == 1f) {\n      return playoutDuration;\n    }\n    return Math.round((double) playoutDuration * speed);\n  }\n\n  /**\n   * Returns the playout duration of {@code mediaDuration} of media.\n   *\n   * @param mediaDuration The duration to scale.\n   * @return The scaled duration, in the same units as {@code mediaDuration}.\n   */\n  public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) {\n    if (speed == 1f) {\n      return mediaDuration;\n    }\n    return Math.round((double) mediaDuration / speed);\n  }\n\n  /**\n   * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate\n   * sync points.\n   *\n   * @param positionUs The requested seek position, in microseocnds.\n   * @param seekParameters The {@link SeekParameters}.\n   * @param firstSyncUs The first candidate seek point, in micrseconds.\n   * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code\n   *     firstSyncUs} if there's only one candidate.\n   * @return The resolved seek position, in microseconds.\n   */\n  public static long resolveSeekPositionUs(\n      long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) {\n    if (SeekParameters.EXACT.equals(seekParameters)) {\n      return positionUs;\n    }\n    long minPositionUs =\n        subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);\n    long maxPositionUs =\n        addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);\n    boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs;\n    boolean secondSyncPositionValid =\n        minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs;\n    if (firstSyncPositionValid && secondSyncPositionValid) {\n      if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) {\n        return firstSyncUs;\n      } else {\n        return secondSyncUs;\n      }\n    } else if (firstSyncPositionValid) {\n      return firstSyncUs;\n    } else if (secondSyncPositionValid) {\n      return secondSyncUs;\n    } else {\n      return minPositionUs;\n    }\n  }\n\n  /**\n   * Converts a list of integers to a primitive array.\n   *\n   * @param list A list of integers.\n   * @return The list in array form, or null if the input list was null.\n   */\n  public static int @PolyNull [] toArray(@PolyNull List<Integer> list) {\n    if (list == null) {\n      return null;\n    }\n    int length = list.size();\n    int[] intArray = new int[length];\n    for (int i = 0; i < length; i++) {\n      intArray[i] = list.get(i);\n    }\n    return intArray;\n  }\n\n  /**\n   * Returns the integer equal to the big-endian concatenation of the characters in {@code string}\n   * as bytes. The string must be no more than four characters long.\n   *\n   * @param string A string no more than four characters long.\n   */\n  public static int getIntegerCodeForString(String string) {\n    int length = string.length();\n    Assertions.checkArgument(length <= 4);\n    int result = 0;\n    for (int i = 0; i < length; i++) {\n      result <<= 8;\n      result |= string.charAt(i);\n    }\n    return result;\n  }\n\n  /**\n   * Converts an integer to a long by unsigned conversion.\n   *\n   * <p>This method is equivalent to {@link Integer#toUnsignedLong(int)} for API 26+.\n   */\n  public static long toUnsignedLong(int x) {\n    // x is implicitly casted to a long before the bit operation is executed but this does not\n    // impact the method correctness.\n    return x & 0xFFFFFFFFL;\n  }\n\n  /**\n   * Returns a byte array containing values parsed from the hex string provided.\n   *\n   * @param hexString The hex string to convert to bytes.\n   * @return A byte array containing values parsed from the hex string provided.\n   */\n  public static byte[] getBytesFromHexString(String hexString) {\n    byte[] data = new byte[hexString.length() / 2];\n    for (int i = 0; i < data.length; i++) {\n      int stringOffset = i * 2;\n      data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)\n          + Character.digit(hexString.charAt(stringOffset + 1), 16));\n    }\n    return data;\n  }\n\n  /**\n   * Returns a string with comma delimited simple names of each object's class.\n   *\n   * @param objects The objects whose simple class names should be comma delimited and returned.\n   * @return A string with comma delimited simple names of each object's class.\n   */\n  public static String getCommaDelimitedSimpleClassNames(Object[] objects) {\n    StringBuilder stringBuilder = new StringBuilder();\n    for (int i = 0; i < objects.length; i++) {\n      stringBuilder.append(objects[i].getClass().getSimpleName());\n      if (i < objects.length - 1) {\n        stringBuilder.append(\", \");\n      }\n    }\n    return stringBuilder.toString();\n  }\n\n  /**\n   * Returns a user agent string based on the given application name and the library version.\n   *\n   * @param context A valid context of the calling application.\n   * @param applicationName String that will be prefix'ed to the generated user agent.\n   * @return A user agent string generated using the applicationName and the library version.\n   */\n  public static String getUserAgent(Context context, String applicationName) {\n    String versionName;\n    try {\n      String packageName = context.getPackageName();\n      PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);\n      versionName = info.versionName;\n    } catch (NameNotFoundException e) {\n      versionName = \"?\";\n    }\n    return applicationName + \"/\" + versionName + \" (Linux;Android \" + Build.VERSION.RELEASE\n        + \") \" + ExoPlayerLibraryInfo.VERSION_SLASHY;\n  }\n\n  /**\n   * Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code\n   * trackType}.\n   *\n   * @param codecs A codec sequence string, as defined in RFC 6381.\n   * @param trackType One of {@link C}{@code .TRACK_TYPE_*}.\n   * @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code\n   *     trackType}. If this ends up empty, or {@code codecs} is null, return null.\n   */\n  public static @Nullable String getCodecsOfType(@Nullable String codecs, int trackType) {\n    String[] codecArray = splitCodecs(codecs);\n    if (codecArray.length == 0) {\n      return null;\n    }\n    StringBuilder builder = new StringBuilder();\n    for (String codec : codecArray) {\n      if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {\n        if (builder.length() > 0) {\n          builder.append(\",\");\n        }\n        builder.append(codec);\n      }\n    }\n    return builder.length() > 0 ? builder.toString() : null;\n  }\n\n  /**\n   * Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings.\n   *\n   * @param codecs A codec sequence string, as defined in RFC 6381.\n   * @return The split codecs, or an array of length zero if the input was empty or null.\n   */\n  public static String[] splitCodecs(@Nullable String codecs) {\n    if (TextUtils.isEmpty(codecs)) {\n      return new String[0];\n    }\n    return split(codecs.trim(), \"(\\\\s*,\\\\s*)\");\n  }\n\n  /**\n   * Converts a sample bit depth to a corresponding PCM encoding constant.\n   *\n   * @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32.\n   * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT},\n   *     {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and\n   *     {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then\n   *     {@link C#ENCODING_INVALID} is returned.\n   */\n  @C.PcmEncoding\n  public static int getPcmEncoding(int bitDepth) {\n    switch (bitDepth) {\n      case 8:\n        return C.ENCODING_PCM_8BIT;\n      case 16:\n        return C.ENCODING_PCM_16BIT;\n      case 24:\n        return C.ENCODING_PCM_24BIT;\n      case 32:\n        return C.ENCODING_PCM_32BIT;\n      default:\n        return C.ENCODING_INVALID;\n    }\n  }\n\n  /**\n   * Returns whether {@code encoding} is one of the linear PCM encodings.\n   *\n   * @param encoding The encoding of the audio data.\n   * @return Whether the encoding is one of the PCM encodings.\n   */\n  public static boolean isEncodingLinearPcm(@C.Encoding int encoding) {\n    return encoding == C.ENCODING_PCM_8BIT\n        || encoding == C.ENCODING_PCM_16BIT\n        || encoding == C.ENCODING_PCM_24BIT\n        || encoding == C.ENCODING_PCM_32BIT\n        || encoding == C.ENCODING_PCM_FLOAT;\n  }\n\n  /**\n   * Returns whether {@code encoding} is high resolution (&gt; 16-bit) integer PCM.\n   *\n   * @param encoding The encoding of the audio data.\n   * @return Whether the encoding is high resolution integer PCM.\n   */\n  public static boolean isEncodingHighResolutionIntegerPcm(@C.PcmEncoding int encoding) {\n    return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT;\n  }\n\n  /**\n   * Returns the audio track channel configuration for the given channel count, or {@link\n   * AudioFormat#CHANNEL_INVALID} if output is not poossible.\n   *\n   * @param channelCount The number of channels in the input audio.\n   * @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not\n   *     possible.\n   */\n  public static int getAudioTrackChannelConfig(int channelCount) {\n    switch (channelCount) {\n      case 1:\n        return AudioFormat.CHANNEL_OUT_MONO;\n      case 2:\n        return AudioFormat.CHANNEL_OUT_STEREO;\n      case 3:\n        return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;\n      case 4:\n        return AudioFormat.CHANNEL_OUT_QUAD;\n      case 5:\n        return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;\n      case 6:\n        return AudioFormat.CHANNEL_OUT_5POINT1;\n      case 7:\n        return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;\n      case 8:\n        if (Util.SDK_INT >= 23) {\n          return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;\n        } else if (Util.SDK_INT >= 21) {\n          // Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.\n          return AudioFormat.CHANNEL_OUT_5POINT1\n              | AudioFormat.CHANNEL_OUT_SIDE_LEFT\n              | AudioFormat.CHANNEL_OUT_SIDE_RIGHT;\n        } else {\n          // 8 ch output is not supported before Android L.\n          return AudioFormat.CHANNEL_INVALID;\n        }\n      default:\n        return AudioFormat.CHANNEL_INVALID;\n    }\n  }\n\n  /**\n   * Returns the frame size for audio with {@code channelCount} channels in the specified encoding.\n   *\n   * @param pcmEncoding The encoding of the audio data.\n   * @param channelCount The channel count.\n   * @return The size of one audio frame in bytes.\n   */\n  public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) {\n    switch (pcmEncoding) {\n      case C.ENCODING_PCM_8BIT:\n        return channelCount;\n      case C.ENCODING_PCM_16BIT:\n        return channelCount * 2;\n      case C.ENCODING_PCM_24BIT:\n        return channelCount * 3;\n      case C.ENCODING_PCM_32BIT:\n      case C.ENCODING_PCM_FLOAT:\n        return channelCount * 4;\n      case C.ENCODING_PCM_A_LAW:\n      case C.ENCODING_PCM_MU_LAW:\n      case C.ENCODING_INVALID:\n      case Format.NO_VALUE:\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  /**\n   * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}.\n   */\n  @C.AudioUsage\n  public static int getAudioUsageForStreamType(@C.StreamType int streamType) {\n    switch (streamType) {\n      case C.STREAM_TYPE_ALARM:\n        return C.USAGE_ALARM;\n      case C.STREAM_TYPE_DTMF:\n        return C.USAGE_VOICE_COMMUNICATION_SIGNALLING;\n      case C.STREAM_TYPE_NOTIFICATION:\n        return C.USAGE_NOTIFICATION;\n      case C.STREAM_TYPE_RING:\n        return C.USAGE_NOTIFICATION_RINGTONE;\n      case C.STREAM_TYPE_SYSTEM:\n        return C.USAGE_ASSISTANCE_SONIFICATION;\n      case C.STREAM_TYPE_VOICE_CALL:\n        return C.USAGE_VOICE_COMMUNICATION;\n      case C.STREAM_TYPE_USE_DEFAULT:\n      case C.STREAM_TYPE_MUSIC:\n      default:\n        return C.USAGE_MEDIA;\n    }\n  }\n\n  /**\n   * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}.\n   */\n  @C.AudioContentType\n  public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) {\n    switch (streamType) {\n      case C.STREAM_TYPE_ALARM:\n      case C.STREAM_TYPE_DTMF:\n      case C.STREAM_TYPE_NOTIFICATION:\n      case C.STREAM_TYPE_RING:\n      case C.STREAM_TYPE_SYSTEM:\n        return C.CONTENT_TYPE_SONIFICATION;\n      case C.STREAM_TYPE_VOICE_CALL:\n        return C.CONTENT_TYPE_SPEECH;\n      case C.STREAM_TYPE_USE_DEFAULT:\n      case C.STREAM_TYPE_MUSIC:\n      default:\n        return C.CONTENT_TYPE_MUSIC;\n    }\n  }\n\n  /**\n   * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}.\n   */\n  @C.StreamType\n  public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) {\n    switch (usage) {\n      case C.USAGE_MEDIA:\n      case C.USAGE_GAME:\n      case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:\n        return C.STREAM_TYPE_MUSIC;\n      case C.USAGE_ASSISTANCE_SONIFICATION:\n        return C.STREAM_TYPE_SYSTEM;\n      case C.USAGE_VOICE_COMMUNICATION:\n        return C.STREAM_TYPE_VOICE_CALL;\n      case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:\n        return C.STREAM_TYPE_DTMF;\n      case C.USAGE_ALARM:\n        return C.STREAM_TYPE_ALARM;\n      case C.USAGE_NOTIFICATION_RINGTONE:\n        return C.STREAM_TYPE_RING;\n      case C.USAGE_NOTIFICATION:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:\n      case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:\n      case C.USAGE_NOTIFICATION_EVENT:\n        return C.STREAM_TYPE_NOTIFICATION;\n      case C.USAGE_ASSISTANCE_ACCESSIBILITY:\n      case C.USAGE_ASSISTANT:\n      case C.USAGE_UNKNOWN:\n      default:\n        return C.STREAM_TYPE_DEFAULT;\n    }\n  }\n\n  /**\n   * Derives a DRM {@link UUID} from {@code drmScheme}.\n   *\n   * @param drmScheme A UUID string, or {@code \"widevine\"}, {@code \"playready\"} or {@code\n   *     \"clearkey\"}.\n   * @return The derived {@link UUID}, or {@code null} if one could not be derived.\n   */\n  public static @Nullable UUID getDrmUuid(String drmScheme) {\n    switch (toLowerInvariant(drmScheme)) {\n      case \"widevine\":\n        return C.WIDEVINE_UUID;\n      case \"playready\":\n        return C.PLAYREADY_UUID;\n      case \"clearkey\":\n        return C.CLEARKEY_UUID;\n      default:\n        try {\n          return UUID.fromString(drmScheme);\n        } catch (RuntimeException e) {\n          return null;\n        }\n    }\n  }\n\n  /**\n   * Makes a best guess to infer the type from a {@link Uri}.\n   *\n   * @param uri The {@link Uri}.\n   * @param overrideExtension If not null, used to infer the type.\n   * @return The content type.\n   */\n  @C.ContentType\n  public static int inferContentType(Uri uri, @Nullable String overrideExtension) {\n    return TextUtils.isEmpty(overrideExtension)\n        ? inferContentType(uri)\n        : inferContentType(\".\" + overrideExtension);\n  }\n\n  /**\n   * Makes a best guess to infer the type from a {@link Uri}.\n   *\n   * @param uri The {@link Uri}.\n   * @return The content type.\n   */\n  @C.ContentType\n  public static int inferContentType(Uri uri) {\n    String path = uri.getPath();\n    return path == null ? C.TYPE_OTHER : inferContentType(path);\n  }\n\n  /**\n   * Makes a best guess to infer the type from a file name.\n   *\n   * @param fileName Name of the file. It can include the path of the file.\n   * @return The content type.\n   */\n  @C.ContentType\n  public static int inferContentType(String fileName) {\n    fileName = toLowerInvariant(fileName);\n    if (fileName.endsWith(\".mpd\")) {\n      return C.TYPE_DASH;\n    } else if (fileName.endsWith(\".m3u8\")) {\n      return C.TYPE_HLS;\n    } else if (fileName.matches(\".*\\\\.ism(l)?(/manifest(\\\\(.+\\\\))?)?\")) {\n      return C.TYPE_SS;\n    } else {\n      return C.TYPE_OTHER;\n    }\n  }\n\n  /**\n   * Returns the specified millisecond time formatted as a string.\n   *\n   * @param builder The builder that {@code formatter} will write to.\n   * @param formatter The formatter.\n   * @param timeMs The time to format as a string, in milliseconds.\n   * @return The time formatted as a string.\n   */\n  public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {\n    if (timeMs == C.TIME_UNSET) {\n      timeMs = 0;\n    }\n    long totalSeconds = (timeMs + 500) / 1000;\n    long seconds = totalSeconds % 60;\n    long minutes = (totalSeconds / 60) % 60;\n    long hours = totalSeconds / 3600;\n    builder.setLength(0);\n    return hours > 0 ? formatter.format(\"%d:%02d:%02d\", hours, minutes, seconds).toString()\n        : formatter.format(\"%02d:%02d\", minutes, seconds).toString();\n  }\n\n  /**\n   * Escapes a string so that it's safe for use as a file or directory name on at least FAT32\n   * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.\n   *\n   * <p>For simplicity, this only handles common characters known to be illegal on FAT32:\n   * &lt;, &gt;, :, \", /, \\, |, ?, and *. % is also escaped since it is used as the escape\n   * character. Escaping is performed in a consistent way so that no collisions occur and\n   * {@link #unescapeFileName(String)} can be used to retrieve the original file name.\n   *\n   * @param fileName File name to be escaped.\n   * @return An escaped file name which will be safe for use on at least FAT32 filesystems.\n   */\n  public static String escapeFileName(String fileName) {\n    int length = fileName.length();\n    int charactersToEscapeCount = 0;\n    for (int i = 0; i < length; i++) {\n      if (shouldEscapeCharacter(fileName.charAt(i))) {\n        charactersToEscapeCount++;\n      }\n    }\n    if (charactersToEscapeCount == 0) {\n      return fileName;\n    }\n\n    int i = 0;\n    StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);\n    while (charactersToEscapeCount > 0) {\n      char c = fileName.charAt(i++);\n      if (shouldEscapeCharacter(c)) {\n        builder.append('%').append(Integer.toHexString(c));\n        charactersToEscapeCount--;\n      } else {\n        builder.append(c);\n      }\n    }\n    if (i < length) {\n      builder.append(fileName, i, length);\n    }\n    return builder.toString();\n  }\n\n  private static boolean shouldEscapeCharacter(char c) {\n    switch (c) {\n      case '<':\n      case '>':\n      case ':':\n      case '\"':\n      case '/':\n      case '\\\\':\n      case '|':\n      case '?':\n      case '*':\n      case '%':\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  /**\n   * Unescapes an escaped file or directory name back to its original value.\n   *\n   * <p>See {@link #escapeFileName(String)} for more information.\n   *\n   * @param fileName File name to be unescaped.\n   * @return The original value of the file name before it was escaped, or null if the escaped\n   *     fileName seems invalid.\n   */\n  public static @Nullable String unescapeFileName(String fileName) {\n    int length = fileName.length();\n    int percentCharacterCount = 0;\n    for (int i = 0; i < length; i++) {\n      if (fileName.charAt(i) == '%') {\n        percentCharacterCount++;\n      }\n    }\n    if (percentCharacterCount == 0) {\n      return fileName;\n    }\n\n    int expectedLength = length - percentCharacterCount * 2;\n    StringBuilder builder = new StringBuilder(expectedLength);\n    Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);\n    int startOfNotEscaped = 0;\n    while (percentCharacterCount > 0 && matcher.find()) {\n      char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);\n      builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);\n      startOfNotEscaped = matcher.end();\n      percentCharacterCount--;\n    }\n    if (startOfNotEscaped < length) {\n      builder.append(fileName, startOfNotEscaped, length);\n    }\n    if (builder.length() != expectedLength) {\n      return null;\n    }\n    return builder.toString();\n  }\n\n  /**\n   * A hacky method that always throws {@code t} even if {@code t} is a checked exception,\n   * and is not declared to be thrown.\n   */\n  public static void sneakyThrow(Throwable t) {\n    sneakyThrowInternal(t);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {\n    throw (T) t;\n  }\n\n  /** Recursively deletes a directory and its content. */\n  public static void recursiveDelete(File fileOrDirectory) {\n    File[] directoryFiles = fileOrDirectory.listFiles();\n    if (directoryFiles != null) {\n      for (File child : directoryFiles) {\n        recursiveDelete(child);\n      }\n    }\n    fileOrDirectory.delete();\n  }\n\n  /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */\n  public static File createTempDirectory(Context context, String prefix) throws IOException {\n    File tempFile = createTempFile(context, prefix);\n    tempFile.delete(); // Delete the temp file.\n    tempFile.mkdir(); // Create a directory with the same name.\n    return tempFile;\n  }\n\n  /** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */\n  public static File createTempFile(Context context, String prefix) throws IOException {\n    return File.createTempFile(prefix, null, context.getCacheDir());\n  }\n\n  /**\n   * Returns the result of updating a CRC-32 with the specified bytes in a \"most significant bit\n   * first\" order.\n   *\n   * @param bytes Array containing the bytes to update the crc value with.\n   * @param start The index to the first byte in the byte range to update the crc with.\n   * @param end The index after the last byte in the byte range to update the crc with.\n   * @param initialValue The initial value for the crc calculation.\n   * @return The result of updating the initial value with the specified bytes.\n   */\n  public static int crc32(byte[] bytes, int start, int end, int initialValue) {\n    for (int i = start; i < end; i++) {\n      initialValue = (initialValue << 8)\n          ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF];\n    }\n    return initialValue;\n  }\n\n  /**\n   * Returns the result of updating a CRC-8 with the specified bytes in a \"most significant bit\n   * first\" order.\n   *\n   * @param bytes Array containing the bytes to update the crc value with.\n   * @param start The index to the first byte in the byte range to update the crc with.\n   * @param end The index after the last byte in the byte range to update the crc with.\n   * @param initialValue The initial value for the crc calculation.\n   * @return The result of updating the initial value with the specified bytes.\n   */\n  public static int crc8(byte[] bytes, int start, int end, int initialValue) {\n    for (int i = start; i < end; i++) {\n      initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)];\n    }\n    return initialValue;\n  }\n\n  /**\n   * Returns the {@link C.NetworkType} of the current network connection.\n   *\n   * @param context A context to access the connectivity manager.\n   * @return The {@link C.NetworkType} of the current network connection.\n   */\n  @C.NetworkType\n  public static int getNetworkType(Context context) {\n    if (context == null) {\n      // Note: This is for backward compatibility only (context used to be @Nullable).\n      return C.NETWORK_TYPE_UNKNOWN;\n    }\n    NetworkInfo networkInfo;\n    ConnectivityManager connectivityManager =\n        (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n    if (connectivityManager == null) {\n      return C.NETWORK_TYPE_UNKNOWN;\n    }\n    try {\n      networkInfo = connectivityManager.getActiveNetworkInfo();\n    } catch (SecurityException e) {\n      // Expected if permission was revoked.\n      return C.NETWORK_TYPE_UNKNOWN;\n    }\n    if (networkInfo == null || !networkInfo.isConnected()) {\n      return C.NETWORK_TYPE_OFFLINE;\n    }\n    switch (networkInfo.getType()) {\n      case ConnectivityManager.TYPE_WIFI:\n        return C.NETWORK_TYPE_WIFI;\n      case ConnectivityManager.TYPE_WIMAX:\n        return C.NETWORK_TYPE_4G;\n      case ConnectivityManager.TYPE_MOBILE:\n      case ConnectivityManager.TYPE_MOBILE_DUN:\n      case ConnectivityManager.TYPE_MOBILE_HIPRI:\n        return getMobileNetworkType(networkInfo);\n      case ConnectivityManager.TYPE_ETHERNET:\n        return C.NETWORK_TYPE_ETHERNET;\n      default: // VPN, Bluetooth, Dummy.\n        return C.NETWORK_TYPE_OTHER;\n    }\n  }\n\n  /**\n   * Returns the upper-case ISO 3166-1 alpha-2 country code of the current registered operator's MCC\n   * (Mobile Country Code), or the country code of the default Locale if not available.\n   *\n   * @param context A context to access the telephony service. If null, only the Locale can be used.\n   * @return The upper-case ISO 3166-1 alpha-2 country code, or an empty String if unavailable.\n   */\n  public static String getCountryCode(@Nullable Context context) {\n    if (context != null) {\n      TelephonyManager telephonyManager =\n          (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);\n      if (telephonyManager != null) {\n        String countryCode = telephonyManager.getNetworkCountryIso();\n        if (!TextUtils.isEmpty(countryCode)) {\n          return toUpperInvariant(countryCode);\n        }\n      }\n    }\n    return toUpperInvariant(Locale.getDefault().getCountry());\n  }\n\n  /**\n   * Returns a non-empty array of normalized IETF BCP 47 language tags for the system languages\n   * ordered by preference.\n   */\n  public static String[] getSystemLanguageCodes() {\n    String[] systemLocales = getSystemLocales();\n    for (int i = 0; i < systemLocales.length; i++) {\n      systemLocales[i] = normalizeLanguageCode(systemLocales[i]);\n    }\n    return systemLocales;\n  }\n\n  /**\n   * Uncompresses the data in {@code input}.\n   *\n   * @param input Wraps the compressed input data.\n   * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code\n   *     output.data} isn't big enough to hold the uncompressed data, a new array is created. If\n   *     {@code true} is returned then the output's position will be set to 0 and its limit will be\n   *     set to the length of the uncompressed data.\n   * @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater}\n   *     is created.\n   * @return Whether the input is uncompressed successfully.\n   */\n  public static boolean inflate(\n      ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) {\n    if (input.bytesLeft() <= 0) {\n      return false;\n    }\n    byte[] outputData = output.data;\n    if (outputData.length < input.bytesLeft()) {\n      outputData = new byte[2 * input.bytesLeft()];\n    }\n    if (inflater == null) {\n      inflater = new Inflater();\n    }\n    inflater.setInput(input.data, input.getPosition(), input.bytesLeft());\n    try {\n      int outputSize = 0;\n      while (true) {\n        outputSize += inflater.inflate(outputData, outputSize, outputData.length - outputSize);\n        if (inflater.finished()) {\n          output.reset(outputData, outputSize);\n          return true;\n        }\n        if (inflater.needsDictionary() || inflater.needsInput()) {\n          return false;\n        }\n        if (outputSize == outputData.length) {\n          outputData = Arrays.copyOf(outputData, outputData.length * 2);\n        }\n      }\n    } catch (DataFormatException e) {\n      return false;\n    } finally {\n      inflater.reset();\n    }\n  }\n\n  /**\n   * Returns whether the app is running on a TV device.\n   *\n   * @param context Any context.\n   * @return Whether the app is running on a TV device.\n   */\n  public static boolean isTv(Context context) {\n    // See https://developer.android.com/training/tv/start/hardware.html#runtime-check.\n    UiModeManager uiModeManager =\n        (UiModeManager) context.getApplicationContext().getSystemService(UI_MODE_SERVICE);\n    return uiModeManager != null\n        && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;\n  }\n\n  /**\n   * Gets the physical size of the default display, in pixels.\n   *\n   * @param context Any context.\n   * @return The physical display size, in pixels.\n   */\n  public static Point getPhysicalDisplaySize(Context context) {\n    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n    return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay());\n  }\n\n  /**\n   * Gets the physical size of the specified display, in pixels.\n   *\n   * @param context Any context.\n   * @param display The display whose size is to be returned.\n   * @return The physical display size, in pixels.\n   */\n  public static Point getPhysicalDisplaySize(Context context, Display display) {\n    if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) {\n      // On Android TVs it is common for the UI to be configured for a lower resolution than\n      // SurfaceViews can output. Before API 26 the Display object does not provide a way to\n      // identify this case, and up to and including API 28 many devices still do not correctly set\n      // their hardware compositor output size.\n\n      // Sony Android TVs advertise support for 4k output via a system feature.\n      if (\"Sony\".equals(Util.MANUFACTURER)\n          && Util.MODEL.startsWith(\"BRAVIA\")\n          && context.getPackageManager().hasSystemFeature(\"com.sony.dtv.hardware.panel.qfhd\")) {\n        return new Point(3840, 2160);\n      }\n\n      // Otherwise check the system property for display size. From API 28 treble may prevent the\n      // system from writing sys.display-size so we check vendor.display-size instead.\n      String displaySize =\n          Util.SDK_INT < 28\n              ? getSystemProperty(\"sys.display-size\")\n              : getSystemProperty(\"vendor.display-size\");\n      // If we managed to read the display size, attempt to parse it.\n      if (!TextUtils.isEmpty(displaySize)) {\n        try {\n          String[] displaySizeParts = split(displaySize.trim(), \"x\");\n          if (displaySizeParts.length == 2) {\n            int width = Integer.parseInt(displaySizeParts[0]);\n            int height = Integer.parseInt(displaySizeParts[1]);\n            if (width > 0 && height > 0) {\n              return new Point(width, height);\n            }\n          }\n        } catch (NumberFormatException e) {\n          // Do nothing.\n        }\n        Log.e(TAG, \"Invalid display size: \" + displaySize);\n      }\n    }\n\n    Point displaySize = new Point();\n    if (Util.SDK_INT >= 23) {\n      getDisplaySizeV23(display, displaySize);\n    } else if (Util.SDK_INT >= 17) {\n      getDisplaySizeV17(display, displaySize);\n    } else {\n      getDisplaySizeV16(display, displaySize);\n    }\n    return displaySize;\n  }\n\n  /**\n   * Extract renderer capabilities for the renderers created by the provided renderers factory.\n   *\n   * @param renderersFactory A {@link RenderersFactory}.\n   * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers.\n   * @return The {@link RendererCapabilities} for each renderer created by the {@code\n   *     renderersFactory}.\n   */\n  public static RendererCapabilities[] getRendererCapabilities(\n      RenderersFactory renderersFactory,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    Renderer[] renderers =\n        renderersFactory.createRenderers(\n            new Handler(),\n            new VideoRendererEventListener() {},\n            new AudioRendererEventListener() {},\n            (cues) -> {},\n            (metadata) -> {},\n            drmSessionManager);\n    RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length];\n    for (int i = 0; i < renderers.length; i++) {\n      capabilities[i] = renderers[i].getCapabilities();\n    }\n    return capabilities;\n  }\n\n  /**\n   * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}.\n   *\n   * @param trackType A {@code TRACK_TYPE_*} constant,\n   * @return A string representation of this constant.\n   */\n  public static String getTrackTypeString(int trackType) {\n    switch (trackType) {\n      case C.TRACK_TYPE_AUDIO:\n        return \"audio\";\n      case C.TRACK_TYPE_DEFAULT:\n        return \"default\";\n      case C.TRACK_TYPE_METADATA:\n        return \"metadata\";\n      case C.TRACK_TYPE_CAMERA_MOTION:\n        return \"camera motion\";\n      case C.TRACK_TYPE_NONE:\n        return \"none\";\n      case C.TRACK_TYPE_TEXT:\n        return \"text\";\n      case C.TRACK_TYPE_VIDEO:\n        return \"video\";\n      default:\n        return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? \"custom (\" + trackType + \")\" : \"?\";\n    }\n  }\n\n  @Nullable\n  private static String getSystemProperty(String name) {\n    try {\n      @SuppressLint(\"PrivateApi\")\n      Class<?> systemProperties = Class.forName(\"android.os.SystemProperties\");\n      Method getMethod = systemProperties.getMethod(\"get\", String.class);\n      return (String) getMethod.invoke(systemProperties, name);\n    } catch (Exception e) {\n      Log.e(TAG, \"Failed to read system property \" + name, e);\n      return null;\n    }\n  }\n\n  @TargetApi(23)\n  private static void getDisplaySizeV23(Display display, Point outSize) {\n    Display.Mode mode = display.getMode();\n    outSize.x = mode.getPhysicalWidth();\n    outSize.y = mode.getPhysicalHeight();\n  }\n\n  @TargetApi(17)\n  private static void getDisplaySizeV17(Display display, Point outSize) {\n    display.getRealSize(outSize);\n  }\n\n  private static void getDisplaySizeV16(Display display, Point outSize) {\n    display.getSize(outSize);\n  }\n\n  private static String[] getSystemLocales() {\n    Configuration config = Resources.getSystem().getConfiguration();\n    return SDK_INT >= 24\n        ? getSystemLocalesV24(config)\n        : new String[] {getLocaleLanguageTag(config.locale)};\n  }\n\n  @TargetApi(24)\n  private static String[] getSystemLocalesV24(Configuration config) {\n    return Util.split(config.getLocales().toLanguageTags(), \",\");\n  }\n\n  @TargetApi(21)\n  private static String getLocaleLanguageTagV21(Locale locale) {\n    return locale.toLanguageTag();\n  }\n\n  @TargetApi(21)\n  private static String normalizeLanguageCodeSyntaxV21(String languageTag) {\n    return Locale.forLanguageTag(languageTag).toLanguageTag();\n  }\n\n  private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {\n    switch (networkInfo.getSubtype()) {\n      case TelephonyManager.NETWORK_TYPE_EDGE:\n      case TelephonyManager.NETWORK_TYPE_GPRS:\n        return C.NETWORK_TYPE_2G;\n      case TelephonyManager.NETWORK_TYPE_1xRTT:\n      case TelephonyManager.NETWORK_TYPE_CDMA:\n      case TelephonyManager.NETWORK_TYPE_EVDO_0:\n      case TelephonyManager.NETWORK_TYPE_EVDO_A:\n      case TelephonyManager.NETWORK_TYPE_EVDO_B:\n      case TelephonyManager.NETWORK_TYPE_HSDPA:\n      case TelephonyManager.NETWORK_TYPE_HSPA:\n      case TelephonyManager.NETWORK_TYPE_HSUPA:\n      case TelephonyManager.NETWORK_TYPE_IDEN:\n      case TelephonyManager.NETWORK_TYPE_UMTS:\n      case TelephonyManager.NETWORK_TYPE_EHRPD:\n      case TelephonyManager.NETWORK_TYPE_HSPAP:\n      case TelephonyManager.NETWORK_TYPE_TD_SCDMA:\n        return C.NETWORK_TYPE_3G;\n      case TelephonyManager.NETWORK_TYPE_LTE:\n        return C.NETWORK_TYPE_4G;\n      case TelephonyManager.NETWORK_TYPE_IWLAN:\n        return C.NETWORK_TYPE_WIFI;\n      case TelephonyManager.NETWORK_TYPE_GSM:\n      case TelephonyManager.NETWORK_TYPE_UNKNOWN:\n      default: // Future mobile network types.\n        return C.NETWORK_TYPE_CELLULAR_UNKNOWN;\n    }\n  }\n\n  private static HashMap<String, String> createIso3ToIso2Map() {\n    String[] iso2Languages = Locale.getISOLanguages();\n    HashMap<String, String> iso3ToIso2 =\n        new HashMap<>(\n            /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length);\n    for (String iso2 : iso2Languages) {\n      try {\n        // This returns the ISO 639-2/T code for the language.\n        String iso3 = new Locale(iso2).getISO3Language();\n        if (!TextUtils.isEmpty(iso3)) {\n          iso3ToIso2.put(iso3, iso2);\n        }\n      } catch (MissingResourceException e) {\n        // Shouldn't happen for list of known languages, but we don't want to throw either.\n      }\n    }\n    // Add additional ISO 639-2/B codes to mapping.\n    for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) {\n      iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]);\n    }\n    return iso3ToIso2;\n  }\n\n  // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.\n  private static final String[] iso3BibliographicalToIso2 =\n      new String[] {\n        \"alb\", \"sq\",\n        \"arm\", \"hy\",\n        \"baq\", \"eu\",\n        \"bur\", \"my\",\n        \"tib\", \"bo\",\n        \"chi\", \"zh\",\n        \"cze\", \"cs\",\n        \"dut\", \"nl\",\n        \"ger\", \"de\",\n        \"gre\", \"el\",\n        \"fre\", \"fr\",\n        \"geo\", \"ka\",\n        \"ice\", \"is\",\n        \"mac\", \"mk\",\n        \"mao\", \"mi\",\n        \"may\", \"ms\",\n        \"per\", \"fa\",\n        \"rum\", \"ro\",\n        \"slo\", \"sk\",\n        \"wel\", \"cy\"\n      };\n\n  /**\n   * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order \"most\n   * significant bit first\".\n   */\n  private static final int[] CRC32_BYTES_MSBF = {\n    0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2,\n    0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3,\n    0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC,\n    0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011,\n    0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E,\n    0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF,\n    0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90,\n    0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95,\n    0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A,\n    0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C,\n    0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13,\n    0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE,\n    0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1,\n    0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20,\n    0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F,\n    0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A,\n    0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055,\n    0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34,\n    0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632,\n    0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F,\n    0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0,\n    0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91,\n    0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E,\n    0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B,\n    0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604,\n    0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615,\n    0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A,\n    0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640,\n    0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F,\n    0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E,\n    0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651,\n    0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654,\n    0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB,\n    0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA,\n    0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5,\n    0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668,\n    0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4\n  };\n\n  /**\n   * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order \"most\n   * significant bit first\".\n   */\n  private static final int[] CRC8_BYTES_MSBF = {\n    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A,\n    0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53,\n    0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4,\n    0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1,\n    0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1,\n    0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88,\n    0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F,\n    0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,\n    0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B,\n    0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2,\n    0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75,\n    0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10,\n    0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40,\n    0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39,\n    0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE,\n    0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,\n    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4,\n    0xF3\n  };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.util;\n\nimport androidx.annotation.Nullable;\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\n\n/**\n * {@link XmlPullParser} utility methods.\n */\npublic final class XmlPullParserUtil {\n\n  private XmlPullParserUtil() {}\n\n  /**\n   * Returns whether the current event is an end tag with the specified name.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @param name The specified name.\n   * @return Whether the current event is an end tag with the specified name.\n   * @throws XmlPullParserException If an error occurs querying the parser.\n   */\n  public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {\n    return isEndTag(xpp) && xpp.getName().equals(name);\n  }\n\n  /**\n   * Returns whether the current event is an end tag.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @return Whether the current event is an end tag.\n   * @throws XmlPullParserException If an error occurs querying the parser.\n   */\n  public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException {\n    return xpp.getEventType() == XmlPullParser.END_TAG;\n  }\n\n  /**\n   * Returns whether the current event is a start tag with the specified name.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @param name The specified name.\n   * @return Whether the current event is a start tag with the specified name.\n   * @throws XmlPullParserException If an error occurs querying the parser.\n   */\n  public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {\n    return isStartTag(xpp) && xpp.getName().equals(name);\n  }\n\n  /**\n   * Returns whether the current event is a start tag.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @return Whether the current event is a start tag.\n   * @throws XmlPullParserException If an error occurs querying the parser.\n   */\n  public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {\n    return xpp.getEventType() == XmlPullParser.START_TAG;\n  }\n\n  /**\n   * Returns whether the current event is a start tag with the specified name. If the current event\n   * has a raw name then its prefix is stripped before matching.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @param name The specified name.\n   * @return Whether the current event is a start tag with the specified name.\n   * @throws XmlPullParserException If an error occurs querying the parser.\n   */\n  public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name)\n      throws XmlPullParserException {\n    return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name);\n  }\n\n  /**\n   * Returns the value of an attribute of the current start tag.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @param attributeName The name of the attribute.\n   * @return The value of the attribute, or null if the current event is not a start tag or if no\n   *     such attribute was found.\n   */\n  public static @Nullable String getAttributeValue(XmlPullParser xpp, String attributeName) {\n    int attributeCount = xpp.getAttributeCount();\n    for (int i = 0; i < attributeCount; i++) {\n      if (xpp.getAttributeName(i).equals(attributeName)) {\n        return xpp.getAttributeValue(i);\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Returns the value of an attribute of the current start tag. Any raw attribute names in the\n   * current start tag have their prefixes stripped before matching.\n   *\n   * @param xpp The {@link XmlPullParser} to query.\n   * @param attributeName The name of the attribute.\n   * @return The value of the attribute, or null if the current event is not a start tag or if no\n   *     such attribute was found.\n   */\n  public static @Nullable String getAttributeValueIgnorePrefix(\n      XmlPullParser xpp, String attributeName) {\n    int attributeCount = xpp.getAttributeCount();\n    for (int i = 0; i < attributeCount; i++) {\n      if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) {\n        return xpp.getAttributeValue(i);\n      }\n    }\n    return null;\n  }\n\n  private static String stripPrefix(String name) {\n    int prefixSeparatorIndex = name.indexOf(':');\n    return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/util/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.util;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.util.CodecSpecificDataUtil;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.NalUnitUtil.SpsData;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * AVC configuration data.\n */\npublic final class AvcConfig {\n\n  public final List<byte[]> initializationData;\n  public final int nalUnitLengthFieldLength;\n  public final int width;\n  public final int height;\n  public final float pixelWidthAspectRatio;\n\n  /**\n   * Parses AVC configuration data.\n   *\n   * @param data A {@link ParsableByteArray}, whose position is set to the start of the AVC\n   *     configuration data to parse.\n   * @return A parsed representation of the HEVC configuration data.\n   * @throws ParserException If an error occurred parsing the data.\n   */\n  public static AvcConfig parse(ParsableByteArray data) throws ParserException {\n    try {\n      data.skipBytes(4); // Skip to the AVCDecoderConfigurationRecord (defined in 14496-15)\n      int nalUnitLengthFieldLength = (data.readUnsignedByte() & 0x3) + 1;\n      if (nalUnitLengthFieldLength == 3) {\n        throw new IllegalStateException();\n      }\n      List<byte[]> initializationData = new ArrayList<>();\n      int numSequenceParameterSets = data.readUnsignedByte() & 0x1F;\n      for (int j = 0; j < numSequenceParameterSets; j++) {\n        initializationData.add(buildNalUnitForChild(data));\n      }\n      int numPictureParameterSets = data.readUnsignedByte();\n      for (int j = 0; j < numPictureParameterSets; j++) {\n        initializationData.add(buildNalUnitForChild(data));\n      }\n\n      int width = Format.NO_VALUE;\n      int height = Format.NO_VALUE;\n      float pixelWidthAspectRatio = 1;\n      if (numSequenceParameterSets > 0) {\n        byte[] sps = initializationData.get(0);\n        SpsData spsData = NalUnitUtil.parseSpsNalUnit(initializationData.get(0),\n            nalUnitLengthFieldLength, sps.length);\n        width = spsData.width;\n        height = spsData.height;\n        pixelWidthAspectRatio = spsData.pixelWidthAspectRatio;\n      }\n      return new AvcConfig(initializationData, nalUnitLengthFieldLength, width, height,\n          pixelWidthAspectRatio);\n    } catch (ArrayIndexOutOfBoundsException e) {\n      throw new ParserException(\"Error parsing AVC config\", e);\n    }\n  }\n\n  private AvcConfig(List<byte[]> initializationData, int nalUnitLengthFieldLength,\n      int width, int height, float pixelWidthAspectRatio) {\n    this.initializationData = initializationData;\n    this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;\n    this.width = width;\n    this.height = height;\n    this.pixelWidthAspectRatio = pixelWidthAspectRatio;\n  }\n\n  private static byte[] buildNalUnitForChild(ParsableByteArray data) {\n    int length = data.readUnsignedShort();\n    int offset = data.getPosition();\n    data.skipBytes(length);\n    return CodecSpecificDataUtil.buildNalUnit(data.data, offset, length);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Stores color info.\n */\npublic final class ColorInfo implements Parcelable {\n\n  /**\n   * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link\n   * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.\n   */\n  @C.ColorSpace\n  public final int colorSpace;\n\n  /**\n   * The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link\n   * C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown.\n   */\n  @C.ColorRange\n  public final int colorRange;\n\n  /**\n   * The color transfer characteristicks of the video. Valid values are {@link\n   * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link\n   * Format#NO_VALUE} if unknown.\n   */\n  @C.ColorTransfer\n  public final int colorTransfer;\n\n  /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */\n  @Nullable public final byte[] hdrStaticInfo;\n\n  // Lazily initialized hashcode.\n  private int hashCode;\n\n  /**\n   * Constructs the ColorInfo.\n   *\n   * @param colorSpace The color space of the video.\n   * @param colorRange The color range of the video.\n   * @param colorTransfer The color transfer characteristics of the video.\n   * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified.\n   */\n  public ColorInfo(\n      @C.ColorSpace int colorSpace,\n      @C.ColorRange int colorRange,\n      @C.ColorTransfer int colorTransfer,\n      @Nullable byte[] hdrStaticInfo) {\n    this.colorSpace = colorSpace;\n    this.colorRange = colorRange;\n    this.colorTransfer = colorTransfer;\n    this.hdrStaticInfo = hdrStaticInfo;\n  }\n\n  @SuppressWarnings(\"ResourceType\")\n  /* package */ ColorInfo(Parcel in) {\n    colorSpace = in.readInt();\n    colorRange = in.readInt();\n    colorTransfer = in.readInt();\n    boolean hasHdrStaticInfo = Util.readBoolean(in);\n    hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null;\n  }\n\n  // Parcelable implementation.\n  @Override\n  public boolean equals(@Nullable Object obj) {\n    if (this == obj) {\n      return true;\n    }\n    if (obj == null || getClass() != obj.getClass()) {\n      return false;\n    }\n    ColorInfo other = (ColorInfo) obj;\n    return colorSpace == other.colorSpace\n        && colorRange == other.colorRange\n        && colorTransfer == other.colorTransfer\n        && Arrays.equals(hdrStaticInfo, other.hdrStaticInfo);\n  }\n\n  @Override\n  public String toString() {\n    return \"ColorInfo(\" + colorSpace + \", \" + colorRange + \", \" + colorTransfer\n        + \", \" + (hdrStaticInfo != null) + \")\";\n  }\n\n  @Override\n  public int hashCode() {\n    if (hashCode == 0) {\n      int result = 17;\n      result = 31 * result + colorSpace;\n      result = 31 * result + colorRange;\n      result = 31 * result + colorTransfer;\n      result = 31 * result + Arrays.hashCode(hdrStaticInfo);\n      hashCode = result;\n    }\n    return hashCode;\n  }\n\n  @Override\n  public int describeContents() {\n    return 0;\n  }\n\n  @Override\n  public void writeToParcel(Parcel dest, int flags) {\n    dest.writeInt(colorSpace);\n    dest.writeInt(colorRange);\n    dest.writeInt(colorTransfer);\n    Util.writeBoolean(dest, hdrStaticInfo != null);\n    if (hdrStaticInfo != null) {\n      dest.writeByteArray(hdrStaticInfo);\n    }\n  }\n\n  public static final Creator<ColorInfo> CREATOR =\n      new Creator<ColorInfo>() {\n        @Override\n        public ColorInfo createFromParcel(Parcel in) {\n          return new ColorInfo(in);\n        }\n\n        @Override\n        public ColorInfo[] newArray(int size) {\n          return new ColorInfo[size];\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\n/** Dolby Vision configuration data. */\npublic final class DolbyVisionConfig {\n\n  /**\n   * Parses Dolby Vision configuration data.\n   *\n   * @param data A {@link ParsableByteArray}, whose position is set to the start of the Dolby Vision\n   *     configuration data to parse.\n   * @return The {@link DolbyVisionConfig} corresponding to the configuration, or {@code null} if\n   *     the configuration isn't supported.\n   */\n  @Nullable\n  public static DolbyVisionConfig parse(ParsableByteArray data) {\n    data.skipBytes(2); // dv_version_major, dv_version_minor\n    int profileData = data.readUnsignedByte();\n    int dvProfile = (profileData >> 1);\n    int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F);\n    String codecsPrefix;\n    if (dvProfile == 4 || dvProfile == 5 || dvProfile == 7) {\n      codecsPrefix = \"dvhe\";\n    } else if (dvProfile == 8) {\n      codecsPrefix = \"hev1\";\n    } else if (dvProfile == 9) {\n      codecsPrefix = \"avc3\";\n    } else {\n      return null;\n    }\n    String codecs = codecsPrefix + \".0\" + dvProfile + \".0\" + dvLevel;\n    return new DolbyVisionConfig(dvProfile, dvLevel, codecs);\n  }\n\n  /** The profile number. */\n  public final int profile;\n  /** The level number. */\n  public final int level;\n  /** The RFC 6381 codecs string. */\n  public final String codecs;\n\n  private DolbyVisionConfig(int profile, int level, String codecs) {\n    this.profile = profile;\n    this.level = level;\n    this.codecs = codecs;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/DummySurface.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE;\nimport static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER;\nimport static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.EGL14;\nimport android.opengl.EGLDisplay;\nimport android.os.Handler;\nimport android.os.Handler.Callback;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport android.view.Surface;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.EGLSurfaceTexture;\nimport com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport javax.microedition.khronos.egl.EGL10;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * A dummy {@link Surface}.\n */\n@TargetApi(17)\npublic final class DummySurface extends Surface {\n\n  private static final String TAG = \"DummySurface\";\n\n  private static final String EXTENSION_PROTECTED_CONTENT = \"EGL_EXT_protected_content\";\n  private static final String EXTENSION_SURFACELESS_CONTEXT = \"EGL_KHR_surfaceless_context\";\n\n  /**\n   * Whether the surface is secure.\n   */\n  public final boolean secure;\n\n  private static @SecureMode int secureMode;\n  private static boolean secureModeInitialized;\n\n  private final DummySurfaceThread thread;\n  private boolean threadReleased;\n\n  /**\n   * Returns whether the device supports secure dummy surfaces.\n   *\n   * @param context Any {@link Context}.\n   * @return Whether the device supports secure dummy surfaces.\n   */\n  public static synchronized boolean isSecureSupported(Context context) {\n    if (!secureModeInitialized) {\n      secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context);\n      secureModeInitialized = true;\n    }\n    return secureMode != SECURE_MODE_NONE;\n  }\n\n  /**\n   * Returns a newly created dummy surface. The surface must be released by calling {@link #release}\n   * when it's no longer required.\n   * <p>\n   * Must only be called if {@link Util#SDK_INT} is 17 or higher.\n   *\n   * @param context Any {@link Context}.\n   * @param secure Whether a secure surface is required. Must only be requested if\n   *     {@link #isSecureSupported(Context)} returns {@code true}.\n   * @throws IllegalStateException If a secure surface is requested on a device for which\n   *     {@link #isSecureSupported(Context)} returns {@code false}.\n   */\n  public static DummySurface newInstanceV17(Context context, boolean secure) {\n    assertApiLevel17OrHigher();\n    Assertions.checkState(!secure || isSecureSupported(context));\n    DummySurfaceThread thread = new DummySurfaceThread();\n    return thread.init(secure ? secureMode : SECURE_MODE_NONE);\n  }\n\n  private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) {\n    super(surfaceTexture);\n    this.thread = thread;\n    this.secure = secure;\n  }\n\n  @Override\n  public void release() {\n    super.release();\n    // The Surface may be released multiple times (explicitly and by Surface.finalize()). The\n    // implementation of super.release() has its own deduplication logic. Below we need to\n    // deduplicate ourselves. Synchronization is required as we don't control the thread on which\n    // Surface.finalize() is called.\n    synchronized (thread) {\n      if (!threadReleased) {\n        thread.release();\n        threadReleased = true;\n      }\n    }\n  }\n\n  private static void assertApiLevel17OrHigher() {\n    if (Util.SDK_INT < 17) {\n      throw new UnsupportedOperationException(\"Unsupported prior to API level 17\");\n    }\n  }\n\n  @TargetApi(24)\n  private static @SecureMode int getSecureModeV24(Context context) {\n    if (Util.SDK_INT < 26 && (\"samsung\".equals(Util.MANUFACTURER) || \"XT1650\".equals(Util.MODEL))) {\n      // Samsung devices running Nougat are known to be broken. See\n      // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802].\n      // Moto Z XT1650 is also affected. See\n      // https://github.com/google/ExoPlayer/issues/3215.\n      return SECURE_MODE_NONE;\n    }\n    if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature(\n        PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {\n      // Pre API level 26 devices were not well tested unless they supported VR mode.\n      return SECURE_MODE_NONE;\n    }\n    EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);\n    String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);\n    if (eglExtensions == null) {\n      return SECURE_MODE_NONE;\n    }\n    if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) {\n      return SECURE_MODE_NONE;\n    }\n    // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may\n    // require support for EXT_protected_surface, but in practice it works on some devices that\n    // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558.\n    return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT)\n        ? SECURE_MODE_SURFACELESS_CONTEXT\n        : SECURE_MODE_PROTECTED_PBUFFER;\n  }\n\n  private static class DummySurfaceThread extends HandlerThread implements Callback {\n\n    private static final int MSG_INIT = 1;\n    private static final int MSG_RELEASE = 2;\n\n    private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture;\n    private @MonotonicNonNull Handler handler;\n    @Nullable private Error initError;\n    @Nullable private RuntimeException initException;\n    @Nullable private DummySurface surface;\n\n    public DummySurfaceThread() {\n      super(\"dummySurface\");\n    }\n\n    public DummySurface init(@SecureMode int secureMode) {\n      start();\n      handler = new Handler(getLooper(), /* callback= */ this);\n      eglSurfaceTexture = new EGLSurfaceTexture(handler);\n      boolean wasInterrupted = false;\n      synchronized (this) {\n        handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();\n        while (surface == null && initException == null && initError == null) {\n          try {\n            wait();\n          } catch (InterruptedException e) {\n            wasInterrupted = true;\n          }\n        }\n      }\n      if (wasInterrupted) {\n        // Restore the interrupted status.\n        Thread.currentThread().interrupt();\n      }\n      if (initException != null) {\n        throw initException;\n      } else if (initError != null) {\n        throw initError;\n      } else {\n        return Assertions.checkNotNull(surface);\n      }\n    }\n\n    public void release() {\n      Assertions.checkNotNull(handler);\n      handler.sendEmptyMessage(MSG_RELEASE);\n    }\n\n    @Override\n    public boolean handleMessage(Message msg) {\n      switch (msg.what) {\n        case MSG_INIT:\n          try {\n            initInternal(/* secureMode= */ msg.arg1);\n          } catch (RuntimeException e) {\n            Log.e(TAG, \"Failed to initialize dummy surface\", e);\n            initException = e;\n          } catch (Error e) {\n            Log.e(TAG, \"Failed to initialize dummy surface\", e);\n            initError = e;\n          } finally {\n            synchronized (this) {\n              notify();\n            }\n          }\n          return true;\n        case MSG_RELEASE:\n          try {\n            releaseInternal();\n          } catch (Throwable e) {\n            Log.e(TAG, \"Failed to release dummy surface\", e);\n          } finally {\n            quit();\n          }\n          return true;\n        default:\n          return true;\n      }\n    }\n\n    private void initInternal(@SecureMode int secureMode) {\n      Assertions.checkNotNull(eglSurfaceTexture);\n      eglSurfaceTexture.init(secureMode);\n      this.surface =\n          new DummySurface(\n              this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);\n    }\n\n    private void releaseInternal() {\n      Assertions.checkNotNull(eglSurfaceTexture);\n      eglSurfaceTexture.release();\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.util.NalUnitUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * HEVC configuration data.\n */\npublic final class HevcConfig {\n\n  @Nullable public final List<byte[]> initializationData;\n  public final int nalUnitLengthFieldLength;\n\n  /**\n   * Parses HEVC configuration data.\n   *\n   * @param data A {@link ParsableByteArray}, whose position is set to the start of the HEVC\n   *     configuration data to parse.\n   * @return A parsed representation of the HEVC configuration data.\n   * @throws ParserException If an error occurred parsing the data.\n   */\n  public static HevcConfig parse(ParsableByteArray data) throws ParserException {\n    try {\n      data.skipBytes(21); // Skip to the NAL unit length size field.\n      int lengthSizeMinusOne = data.readUnsignedByte() & 0x03;\n\n      // Calculate the combined size of all VPS/SPS/PPS bitstreams.\n      int numberOfArrays = data.readUnsignedByte();\n      int csdLength = 0;\n      int csdStartPosition = data.getPosition();\n      for (int i = 0; i < numberOfArrays; i++) {\n        data.skipBytes(1); // completeness (1), nal_unit_type (7)\n        int numberOfNalUnits = data.readUnsignedShort();\n        for (int j = 0; j < numberOfNalUnits; j++) {\n          int nalUnitLength = data.readUnsignedShort();\n          csdLength += 4 + nalUnitLength; // Start code and NAL unit.\n          data.skipBytes(nalUnitLength);\n        }\n      }\n\n      // Concatenate the codec-specific data into a single buffer.\n      data.setPosition(csdStartPosition);\n      byte[] buffer = new byte[csdLength];\n      int bufferPosition = 0;\n      for (int i = 0; i < numberOfArrays; i++) {\n        data.skipBytes(1); // completeness (1), nal_unit_type (7)\n        int numberOfNalUnits = data.readUnsignedShort();\n        for (int j = 0; j < numberOfNalUnits; j++) {\n          int nalUnitLength = data.readUnsignedShort();\n          System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition,\n              NalUnitUtil.NAL_START_CODE.length);\n          bufferPosition += NalUnitUtil.NAL_START_CODE.length;\n          System\n              .arraycopy(data.data, data.getPosition(), buffer, bufferPosition, nalUnitLength);\n          bufferPosition += nalUnitLength;\n          data.skipBytes(nalUnitLength);\n        }\n      }\n\n      List<byte[]> initializationData = csdLength == 0 ? null : Collections.singletonList(buffer);\n      return new HevcConfig(initializationData, lengthSizeMinusOne + 1);\n    } catch (ArrayIndexOutOfBoundsException e) {\n      throw new ParserException(\"Error parsing HEVC config\", e);\n    }\n  }\n\n  private HevcConfig(@Nullable List<byte[]> initializationData, int nalUnitLengthFieldLength) {\n    this.initializationData = initializationData;\n    this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.media.MediaCodec;\nimport android.media.MediaCodecInfo.CodecCapabilities;\nimport android.media.MediaCodecInfo.CodecProfileLevel;\nimport android.media.MediaCrypto;\nimport android.media.MediaFormat;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.util.Pair;\nimport android.view.Surface;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.mediacodec.MediaFormatUtil;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;\nimport java.nio.ByteBuffer;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Decodes and renders video using {@link MediaCodec}.\n *\n * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}\n * on the playback thread:\n *\n * <ul>\n *   <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload\n *       should be the target {@link Surface}, or null.\n *   <li>Message with type {@link C#MSG_SET_SCALING_MODE} to set the video scaling mode. The message\n *       payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that\n *       the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by\n *       a {@link android.view.SurfaceView}.\n * </ul>\n */\npublic class MediaCodecVideoRenderer extends MediaCodecRenderer {\n\n  private static final String TAG = \"MediaCodecVideoRenderer\";\n  private static final String KEY_CROP_LEFT = \"crop-left\";\n  private static final String KEY_CROP_RIGHT = \"crop-right\";\n  private static final String KEY_CROP_BOTTOM = \"crop-bottom\";\n  private static final String KEY_CROP_TOP = \"crop-top\";\n\n  // Long edge length in pixels for standard video formats, in decreasing in order.\n  private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] {\n      1920, 1600, 1440, 1280, 960, 854, 640, 540, 480};\n\n  // Generally there is zero or one pending output stream offset. We track more offsets to allow for\n  // pending output streams that have fewer frames than the codec latency.\n  private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10;\n  /**\n   * Scale factor for the initial maximum input size used to configure the codec in non-adaptive\n   * playbacks. See {@link #getCodecMaxValues(MediaCodecInfo, Format, Format[])}.\n   */\n  private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f;\n\n  /** Magic frame render timestamp that indicates the EOS in tunneling mode. */\n  private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE;\n\n  /** A {@link DecoderException} with additional surface information. */\n  public static final class VideoDecoderException extends DecoderException {\n\n    /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */\n    public final int surfaceIdentityHashCode;\n\n    /** Whether the surface was valid when the exception occurred. */\n    public final boolean isSurfaceValid;\n\n    public VideoDecoderException(\n        Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) {\n      super(cause, codecInfo);\n      surfaceIdentityHashCode = System.identityHashCode(surface);\n      isSurfaceValid = surface == null || surface.isValid();\n    }\n  }\n\n  private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround;\n  private static boolean deviceNeedsSetOutputSurfaceWorkaround;\n\n  private final Context context;\n  private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;\n  private final EventDispatcher eventDispatcher;\n  private final long allowedJoiningTimeMs;\n  private final int maxDroppedFramesToNotify;\n  private final boolean deviceNeedsNoPostProcessWorkaround;\n  private final long[] pendingOutputStreamOffsetsUs;\n  private final long[] pendingOutputStreamSwitchTimesUs;\n\n  private CodecMaxValues codecMaxValues;\n  private boolean codecNeedsSetOutputSurfaceWorkaround;\n  private boolean codecHandlesHdr10PlusOutOfBandMetadata;\n\n  private Surface surface;\n  private Surface dummySurface;\n  @C.VideoScalingMode\n  private int scalingMode;\n  private boolean renderedFirstFrame;\n  private long initialPositionUs;\n  private long joiningDeadlineMs;\n  private long droppedFrameAccumulationStartTimeMs;\n  private int droppedFrames;\n  private int consecutiveDroppedFrameCount;\n  private int buffersInCodecCount;\n  private long lastRenderTimeUs;\n\n  private int pendingRotationDegrees;\n  private float pendingPixelWidthHeightRatio;\n  @Nullable private MediaFormat currentMediaFormat;\n  private int currentWidth;\n  private int currentHeight;\n  private int currentUnappliedRotationDegrees;\n  private float currentPixelWidthHeightRatio;\n  private int reportedWidth;\n  private int reportedHeight;\n  private int reportedUnappliedRotationDegrees;\n  private float reportedPixelWidthHeightRatio;\n\n  private boolean tunneling;\n  private int tunnelingAudioSessionId;\n  /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;\n\n  private long lastInputTimeUs;\n  private long outputStreamOffsetUs;\n  private int pendingOutputStreamOffsetCount;\n  @Nullable private VideoFrameMetadataListener frameMetadataListener;\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   */\n  public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {\n    this(context, mediaCodecSelector, 0);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   */\n  public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,\n      long allowedJoiningTimeMs) {\n    this(\n        context,\n        mediaCodecSelector,\n        allowedJoiningTimeMs,\n        /* eventHandler= */ null,\n        /* eventListener= */ null,\n        /* maxDroppedFramesToNotify= */ -1);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between\n   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.\n   */\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecVideoRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      long allowedJoiningTimeMs,\n      @Nullable Handler eventHandler,\n      @Nullable VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify) {\n    this(\n        context,\n        mediaCodecSelector,\n        allowedJoiningTimeMs,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        eventHandler,\n        eventListener,\n        maxDroppedFramesToNotify);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between\n   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.\n   * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean,\n   *     Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecVideoRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      long allowedJoiningTimeMs,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      @Nullable Handler eventHandler,\n      @Nullable VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify) {\n    this(\n        context,\n        mediaCodecSelector,\n        allowedJoiningTimeMs,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        /* enableDecoderFallback= */ false,\n        eventHandler,\n        eventListener,\n        maxDroppedFramesToNotify);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between\n   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.\n   */\n  @SuppressWarnings(\"deprecation\")\n  public MediaCodecVideoRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      long allowedJoiningTimeMs,\n      boolean enableDecoderFallback,\n      @Nullable Handler eventHandler,\n      @Nullable VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify) {\n    this(\n        context,\n        mediaCodecSelector,\n        allowedJoiningTimeMs,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        enableDecoderFallback,\n        eventHandler,\n        eventListener,\n        maxDroppedFramesToNotify);\n  }\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted\n   *     content is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder\n   *     initialization fails. This may result in using a decoder that is slower/less efficient than\n   *     the primary decoder.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between\n   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.\n   * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean,\n   *     Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the\n   *     {@link MediaSource} factories.\n   */\n  @Deprecated\n  public MediaCodecVideoRenderer(\n      Context context,\n      MediaCodecSelector mediaCodecSelector,\n      long allowedJoiningTimeMs,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean enableDecoderFallback,\n      @Nullable Handler eventHandler,\n      @Nullable VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify) {\n    super(\n        C.TRACK_TYPE_VIDEO,\n        mediaCodecSelector,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        enableDecoderFallback,\n        /* assumedMinimumCodecOperatingRate= */ 30);\n    this.allowedJoiningTimeMs = allowedJoiningTimeMs;\n    this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;\n    this.context = context.getApplicationContext();\n    frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);\n    eventDispatcher = new EventDispatcher(eventHandler, eventListener);\n    deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();\n    pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];\n    pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];\n    outputStreamOffsetUs = C.TIME_UNSET;\n    lastInputTimeUs = C.TIME_UNSET;\n    joiningDeadlineMs = C.TIME_UNSET;\n    currentWidth = Format.NO_VALUE;\n    currentHeight = Format.NO_VALUE;\n    currentPixelWidthHeightRatio = Format.NO_VALUE;\n    pendingPixelWidthHeightRatio = Format.NO_VALUE;\n    scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;\n    clearReportedVideoSize();\n  }\n\n  @Override\n  @Capabilities\n  protected int supportsFormat(\n      MediaCodecSelector mediaCodecSelector,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      Format format)\n      throws DecoderQueryException {\n    String mimeType = format.sampleMimeType;\n    if (!MimeTypes.isVideo(mimeType)) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n    }\n    @Nullable DrmInitData drmInitData = format.drmInitData;\n    // Assume encrypted content requires secure decoders.\n    boolean requiresSecureDecryption = drmInitData != null;\n    List<MediaCodecInfo> decoderInfos =\n        getDecoderInfos(\n            mediaCodecSelector,\n            format,\n            requiresSecureDecryption,\n            /* requiresTunnelingDecoder= */ false);\n    if (requiresSecureDecryption && decoderInfos.isEmpty()) {\n      // No secure decoders are available. Fall back to non-secure decoders.\n      decoderInfos =\n          getDecoderInfos(\n              mediaCodecSelector,\n              format,\n              /* requiresSecureDecoder= */ false,\n              /* requiresTunnelingDecoder= */ false);\n    }\n    if (decoderInfos.isEmpty()) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);\n    }\n    boolean supportsFormatDrm =\n        drmInitData == null\n            || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType)\n            || (format.exoMediaCryptoType == null\n                && supportsFormatDrm(drmSessionManager, drmInitData));\n    if (!supportsFormatDrm) {\n      return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);\n    }\n    // Check capabilities for the first decoder in the list, which takes priority.\n    MediaCodecInfo decoderInfo = decoderInfos.get(0);\n    boolean isFormatSupported = decoderInfo.isFormatSupported(format);\n    @AdaptiveSupport\n    int adaptiveSupport =\n        decoderInfo.isSeamlessAdaptationSupported(format)\n            ? ADAPTIVE_SEAMLESS\n            : ADAPTIVE_NOT_SEAMLESS;\n    @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED;\n    if (isFormatSupported) {\n      List<MediaCodecInfo> tunnelingDecoderInfos =\n          getDecoderInfos(\n              mediaCodecSelector,\n              format,\n              requiresSecureDecryption,\n              /* requiresTunnelingDecoder= */ true);\n      if (!tunnelingDecoderInfos.isEmpty()) {\n        MediaCodecInfo tunnelingDecoderInfo = tunnelingDecoderInfos.get(0);\n        if (tunnelingDecoderInfo.isFormatSupported(format)\n            && tunnelingDecoderInfo.isSeamlessAdaptationSupported(format)) {\n          tunnelingSupport = TUNNELING_SUPPORTED;\n        }\n      }\n    }\n    @FormatSupport\n    int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;\n    return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport);\n  }\n\n  @Override\n  protected List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)\n      throws DecoderQueryException {\n    return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling);\n  }\n\n  private static List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector,\n      Format format,\n      boolean requiresSecureDecoder,\n      boolean requiresTunnelingDecoder)\n      throws DecoderQueryException {\n    @Nullable String mimeType = format.sampleMimeType;\n    if (mimeType == null) {\n      return Collections.emptyList();\n    }\n    List<MediaCodecInfo> decoderInfos =\n        mediaCodecSelector.getDecoderInfos(\n            mimeType, requiresSecureDecoder, requiresTunnelingDecoder);\n    decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format);\n    if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) {\n      // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles.\n      @Nullable\n      Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);\n      if (codecProfileAndLevel != null) {\n        int profile = codecProfileAndLevel.first;\n        if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr\n            || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) {\n          decoderInfos.addAll(\n              mediaCodecSelector.getDecoderInfos(\n                  MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder));\n        } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) {\n          decoderInfos.addAll(\n              mediaCodecSelector.getDecoderInfos(\n                  MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder));\n        }\n      }\n    }\n    return Collections.unmodifiableList(decoderInfos);\n  }\n\n  @Override\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    super.onEnabled(joining);\n    int oldTunnelingAudioSessionId = tunnelingAudioSessionId;\n    tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;\n    tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET;\n    if (tunnelingAudioSessionId != oldTunnelingAudioSessionId) {\n      releaseCodec();\n    }\n    eventDispatcher.enabled(decoderCounters);\n    frameReleaseTimeHelper.enable();\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\n    if (outputStreamOffsetUs == C.TIME_UNSET) {\n      outputStreamOffsetUs = offsetUs;\n    } else {\n      if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) {\n        Log.w(TAG, \"Too many stream changes, so dropping offset: \"\n            + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]);\n      } else {\n        pendingOutputStreamOffsetCount++;\n      }\n      pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs;\n      pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = lastInputTimeUs;\n    }\n    super.onStreamChanged(formats, offsetUs);\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    super.onPositionReset(positionUs, joining);\n    clearRenderedFirstFrame();\n    initialPositionUs = C.TIME_UNSET;\n    consecutiveDroppedFrameCount = 0;\n    lastInputTimeUs = C.TIME_UNSET;\n    if (pendingOutputStreamOffsetCount != 0) {\n      outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1];\n      pendingOutputStreamOffsetCount = 0;\n    }\n    if (joining) {\n      setJoiningDeadlineMs();\n    } else {\n      joiningDeadlineMs = C.TIME_UNSET;\n    }\n  }\n\n  @Override\n  public boolean isReady() {\n    if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface)\n        || getCodec() == null || tunneling)) {\n      // Ready. If we were joining then we've now joined, so clear the joining deadline.\n      joiningDeadlineMs = C.TIME_UNSET;\n      return true;\n    } else if (joiningDeadlineMs == C.TIME_UNSET) {\n      // Not joining.\n      return false;\n    } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) {\n      // Joining and still within the joining deadline.\n      return true;\n    } else {\n      // The joining deadline has been exceeded. Give up and clear the deadline.\n      joiningDeadlineMs = C.TIME_UNSET;\n      return false;\n    }\n  }\n\n  @Override\n  protected void onStarted() {\n    super.onStarted();\n    droppedFrames = 0;\n    droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();\n    lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;\n  }\n\n  @Override\n  protected void onStopped() {\n    joiningDeadlineMs = C.TIME_UNSET;\n    maybeNotifyDroppedFrames();\n    super.onStopped();\n  }\n\n  @Override\n  protected void onDisabled() {\n    lastInputTimeUs = C.TIME_UNSET;\n    outputStreamOffsetUs = C.TIME_UNSET;\n    pendingOutputStreamOffsetCount = 0;\n    currentMediaFormat = null;\n    clearReportedVideoSize();\n    clearRenderedFirstFrame();\n    frameReleaseTimeHelper.disable();\n    tunnelingOnFrameRenderedListener = null;\n    try {\n      super.onDisabled();\n    } finally {\n      eventDispatcher.disabled(decoderCounters);\n    }\n  }\n\n  @Override\n  protected void onReset() {\n    try {\n      super.onReset();\n    } finally {\n      if (dummySurface != null) {\n        if (surface == dummySurface) {\n          surface = null;\n        }\n        dummySurface.release();\n        dummySurface = null;\n      }\n    }\n  }\n\n  @Override\n  public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {\n    if (messageType == C.MSG_SET_SURFACE) {\n      setSurface((Surface) message);\n    } else if (messageType == C.MSG_SET_SCALING_MODE) {\n      scalingMode = (Integer) message;\n      MediaCodec codec = getCodec();\n      if (codec != null) {\n        codec.setVideoScalingMode(scalingMode);\n      }\n    } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) {\n      frameMetadataListener = (VideoFrameMetadataListener) message;\n    } else {\n      super.handleMessage(messageType, message);\n    }\n  }\n\n  private void setSurface(Surface surface) throws ExoPlaybackException {\n    if (surface == null) {\n      // Use a dummy surface if possible.\n      if (dummySurface != null) {\n        surface = dummySurface;\n      } else {\n        MediaCodecInfo codecInfo = getCodecInfo();\n        if (codecInfo != null && shouldUseDummySurface(codecInfo)) {\n          dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);\n          surface = dummySurface;\n        }\n      }\n    }\n    // We only need to update the codec if the surface has changed.\n    if (this.surface != surface) {\n      this.surface = surface;\n      @State int state = getState();\n      MediaCodec codec = getCodec();\n      if (codec != null) {\n        if (Util.SDK_INT >= 23 && surface != null && !codecNeedsSetOutputSurfaceWorkaround) {\n          setOutputSurfaceV23(codec, surface);\n        } else {\n          releaseCodec();\n          maybeInitCodec();\n        }\n      }\n      if (surface != null && surface != dummySurface) {\n        // If we know the video size, report it again immediately.\n        maybeRenotifyVideoSizeChanged();\n        // We haven't rendered to the new surface yet.\n        clearRenderedFirstFrame();\n        if (state == STATE_STARTED) {\n          setJoiningDeadlineMs();\n        }\n      } else {\n        // The surface has been removed.\n        clearReportedVideoSize();\n        clearRenderedFirstFrame();\n      }\n    } else if (surface != null && surface != dummySurface) {\n      // The surface is set and unchanged. If we know the video size and/or have already rendered to\n      // the surface, report these again immediately.\n      maybeRenotifyVideoSizeChanged();\n      maybeRenotifyRenderedFirstFrame();\n    }\n  }\n\n  @Override\n  protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {\n    return surface != null || shouldUseDummySurface(codecInfo);\n  }\n\n  @Override\n  protected boolean getCodecNeedsEosPropagation() {\n    // Since API 23, onFrameRenderedListener allows for detection of the renderer EOS.\n    return tunneling && Util.SDK_INT < 23;\n  }\n\n  @Override\n  protected void configureCodec(\n      MediaCodecInfo codecInfo,\n      MediaCodec codec,\n      Format format,\n      @Nullable MediaCrypto crypto,\n      float codecOperatingRate) {\n    String codecMimeType = codecInfo.codecMimeType;\n    codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());\n    MediaFormat mediaFormat =\n        getMediaFormat(\n            format,\n            codecMimeType,\n            codecMaxValues,\n            codecOperatingRate,\n            deviceNeedsNoPostProcessWorkaround,\n            tunnelingAudioSessionId);\n    if (surface == null) {\n      Assertions.checkState(shouldUseDummySurface(codecInfo));\n      if (dummySurface == null) {\n        dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);\n      }\n      surface = dummySurface;\n    }\n    codec.configure(mediaFormat, surface, crypto, 0);\n    if (Util.SDK_INT >= 23 && tunneling) {\n      tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);\n    }\n  }\n\n  @Override\n  protected @KeepCodecResult int canKeepCodec(\n      MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {\n    if (codecInfo.isSeamlessAdaptationSupported(\n            oldFormat, newFormat, /* isNewFormatComplete= */ true)\n        && newFormat.width <= codecMaxValues.width\n        && newFormat.height <= codecMaxValues.height\n        && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) {\n      return oldFormat.initializationDataEquals(newFormat)\n          ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION\n          : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION;\n    }\n    return KEEP_CODEC_RESULT_NO;\n  }\n\n  @CallSuper\n  @Override\n  protected void releaseCodec() {\n    try {\n      super.releaseCodec();\n    } finally {\n      buffersInCodecCount = 0;\n    }\n  }\n\n  @CallSuper\n  @Override\n  protected boolean flushOrReleaseCodec() {\n    try {\n      return super.flushOrReleaseCodec();\n    } finally {\n      buffersInCodecCount = 0;\n    }\n  }\n\n  @Override\n  protected float getCodecOperatingRateV23(\n      float operatingRate, Format format, Format[] streamFormats) {\n    // Use the highest known stream frame-rate up front, to avoid having to reconfigure the codec\n    // should an adaptive switch to that stream occur.\n    float maxFrameRate = -1;\n    for (Format streamFormat : streamFormats) {\n      float streamFrameRate = streamFormat.frameRate;\n      if (streamFrameRate != Format.NO_VALUE) {\n        maxFrameRate = Math.max(maxFrameRate, streamFrameRate);\n      }\n    }\n    return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * operatingRate);\n  }\n\n  @Override\n  protected void onCodecInitialized(String name, long initializedTimestampMs,\n      long initializationDurationMs) {\n    eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);\n    codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name);\n    codecHandlesHdr10PlusOutOfBandMetadata =\n        Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported();\n  }\n\n  @Override\n  protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {\n    super.onInputFormatChanged(formatHolder);\n    Format newFormat = formatHolder.format;\n    eventDispatcher.inputFormatChanged(newFormat);\n    pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio;\n    pendingRotationDegrees = newFormat.rotationDegrees;\n  }\n\n  /**\n   * Called immediately before an input buffer is queued into the codec.\n   *\n   * @param buffer The buffer to be queued.\n   */\n  @CallSuper\n  @Override\n  protected void onQueueInputBuffer(DecoderInputBuffer buffer) {\n    buffersInCodecCount++;\n    lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs);\n    if (Util.SDK_INT < 23 && tunneling) {\n      // In tunneled mode before API 23 we don't have a way to know when the buffer is output, so\n      // treat it as if it were output immediately.\n      onProcessedTunneledBuffer(buffer.timeUs);\n    }\n  }\n\n  @Override\n  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) {\n    currentMediaFormat = outputMediaFormat;\n    boolean hasCrop =\n        outputMediaFormat.containsKey(KEY_CROP_RIGHT)\n            && outputMediaFormat.containsKey(KEY_CROP_LEFT)\n            && outputMediaFormat.containsKey(KEY_CROP_BOTTOM)\n            && outputMediaFormat.containsKey(KEY_CROP_TOP);\n    int width =\n        hasCrop\n            ? outputMediaFormat.getInteger(KEY_CROP_RIGHT)\n                - outputMediaFormat.getInteger(KEY_CROP_LEFT)\n                + 1\n            : outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH);\n    int height =\n        hasCrop\n            ? outputMediaFormat.getInteger(KEY_CROP_BOTTOM)\n                - outputMediaFormat.getInteger(KEY_CROP_TOP)\n                + 1\n            : outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);\n    processOutputFormat(codec, width, height);\n  }\n\n  @Override\n  protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer)\n      throws ExoPlaybackException {\n    if (!codecHandlesHdr10PlusOutOfBandMetadata) {\n      return;\n    }\n    ByteBuffer data = Assertions.checkNotNull(buffer.supplementalData);\n    if (data.remaining() >= 7) {\n      // Check for HDR10+ out-of-band metadata. See User_data_registered_itu_t_t35 in ST 2094-40.\n      byte ituTT35CountryCode = data.get();\n      int ituTT35TerminalProviderCode = data.getShort();\n      int ituTT35TerminalProviderOrientedCode = data.getShort();\n      byte applicationIdentifier = data.get();\n      byte applicationVersion = data.get();\n      data.position(0);\n      if (ituTT35CountryCode == (byte) 0xB5\n          && ituTT35TerminalProviderCode == 0x003C\n          && ituTT35TerminalProviderOrientedCode == 0x0001\n          && applicationIdentifier == 4\n          && applicationVersion == 0) {\n        // The metadata size may vary so allocate a new array every time. This is not too\n        // inefficient because the metadata is only a few tens of bytes.\n        byte[] hdr10PlusInfo = new byte[data.remaining()];\n        data.get(hdr10PlusInfo);\n        data.position(0);\n        // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build.\n        setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo);\n      }\n    }\n  }\n\n  @Override\n  protected boolean processOutputBuffer(\n      long positionUs,\n      long elapsedRealtimeUs,\n      MediaCodec codec,\n      ByteBuffer buffer,\n      int bufferIndex,\n      int bufferFlags,\n      long bufferPresentationTimeUs,\n      boolean isDecodeOnlyBuffer,\n      boolean isLastBuffer,\n      Format format)\n      throws ExoPlaybackException {\n    if (initialPositionUs == C.TIME_UNSET) {\n      initialPositionUs = positionUs;\n    }\n\n    long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;\n\n    if (isDecodeOnlyBuffer && !isLastBuffer) {\n      skipOutputBuffer(codec, bufferIndex, presentationTimeUs);\n      return true;\n    }\n\n    long earlyUs = bufferPresentationTimeUs - positionUs;\n    if (surface == dummySurface) {\n      // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.\n      if (isBufferLate(earlyUs)) {\n        skipOutputBuffer(codec, bufferIndex, presentationTimeUs);\n        return true;\n      }\n      return false;\n    }\n\n    long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;\n    long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs;\n    boolean isStarted = getState() == STATE_STARTED;\n    // Don't force output until we joined and the position reached the current stream.\n    boolean forceRenderOutputBuffer =\n        joiningDeadlineMs == C.TIME_UNSET\n            && positionUs >= outputStreamOffsetUs\n            && (!renderedFirstFrame\n                || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs)));\n    if (forceRenderOutputBuffer) {\n      long releaseTimeNs = System.nanoTime();\n      notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format, currentMediaFormat);\n      if (Util.SDK_INT >= 21) {\n        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs);\n      } else {\n        renderOutputBuffer(codec, bufferIndex, presentationTimeUs);\n      }\n      return true;\n    }\n\n    if (!isStarted || positionUs == initialPositionUs) {\n      return false;\n    }\n\n    // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current\n    // iteration of the rendering loop.\n    long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs;\n    earlyUs -= elapsedSinceStartOfLoopUs;\n\n    // Compute the buffer's desired release time in nanoseconds.\n    long systemTimeNs = System.nanoTime();\n    long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);\n\n    // Apply a timestamp adjustment, if there is one.\n    long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(\n        bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs);\n    earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;\n\n    boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET;\n    if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)\n        && maybeDropBuffersToKeyframe(\n            codec, bufferIndex, presentationTimeUs, positionUs, treatDroppedBuffersAsSkipped)) {\n      return false;\n    } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {\n      if (treatDroppedBuffersAsSkipped) {\n        skipOutputBuffer(codec, bufferIndex, presentationTimeUs);\n      } else {\n        dropOutputBuffer(codec, bufferIndex, presentationTimeUs);\n      }\n      return true;\n    }\n\n    if (Util.SDK_INT >= 21) {\n      // Let the underlying framework time the release.\n      if (earlyUs < 50000) {\n        notifyFrameMetadataListener(\n            presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat);\n        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);\n        return true;\n      }\n    } else {\n      // We need to time the release ourselves.\n      if (earlyUs < 30000) {\n        if (earlyUs > 11000) {\n          // We're a little too early to render the frame. Sleep until the frame can be rendered.\n          // Note: The 11ms threshold was chosen fairly arbitrarily.\n          try {\n            // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.\n            Thread.sleep((earlyUs - 10000) / 1000);\n          } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            return false;\n          }\n        }\n        notifyFrameMetadataListener(\n            presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat);\n        renderOutputBuffer(codec, bufferIndex, presentationTimeUs);\n        return true;\n      }\n    }\n\n    // We're either not playing, or it's not time to render the frame yet.\n    return false;\n  }\n\n  private void processOutputFormat(MediaCodec codec, int width, int height) {\n    currentWidth = width;\n    currentHeight = height;\n    currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio;\n    if (Util.SDK_INT >= 21) {\n      // On API level 21 and above the decoder applies the rotation when rendering to the surface.\n      // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need\n      // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.\n      if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) {\n        int rotatedHeight = currentWidth;\n        currentWidth = currentHeight;\n        currentHeight = rotatedHeight;\n        currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;\n      }\n    } else {\n      // On API level 20 and below the decoder does not apply the rotation.\n      currentUnappliedRotationDegrees = pendingRotationDegrees;\n    }\n    // Must be applied each time the output MediaFormat changes.\n    codec.setVideoScalingMode(scalingMode);\n  }\n\n  private void notifyFrameMetadataListener(\n      long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) {\n    if (frameMetadataListener != null) {\n      frameMetadataListener.onVideoFrameAboutToBeRendered(\n          presentationTimeUs, releaseTimeNs, format, mediaFormat);\n    }\n  }\n\n  /**\n   * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link\n   * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean,\n   * Format)} to get the playback position with respect to the media.\n   */\n  protected long getOutputStreamOffsetUs() {\n    return outputStreamOffsetUs;\n  }\n\n  /** Called when a buffer was processed in tunneling mode. */\n  protected void onProcessedTunneledBuffer(long presentationTimeUs) {\n    @Nullable Format format = updateOutputFormatForTime(presentationTimeUs);\n    if (format != null) {\n      processOutputFormat(getCodec(), format.width, format.height);\n    }\n    maybeNotifyVideoSizeChanged();\n    maybeNotifyRenderedFirstFrame();\n    onProcessedOutputBuffer(presentationTimeUs);\n  }\n\n  /** Called when a output EOS was received in tunneling mode. */\n  private void onProcessedTunneledEndOfStream() {\n    setPendingOutputEndOfStream();\n  }\n\n  /**\n   * Called when an output buffer is successfully processed.\n   *\n   * @param presentationTimeUs The timestamp associated with the output buffer.\n   */\n  @CallSuper\n  @Override\n  protected void onProcessedOutputBuffer(long presentationTimeUs) {\n    buffersInCodecCount--;\n    while (pendingOutputStreamOffsetCount != 0\n        && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) {\n      outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0];\n      pendingOutputStreamOffsetCount--;\n      System.arraycopy(\n          pendingOutputStreamOffsetsUs,\n          /* srcPos= */ 1,\n          pendingOutputStreamOffsetsUs,\n          /* destPos= */ 0,\n          pendingOutputStreamOffsetCount);\n      System.arraycopy(\n          pendingOutputStreamSwitchTimesUs,\n          /* srcPos= */ 1,\n          pendingOutputStreamSwitchTimesUs,\n          /* destPos= */ 0,\n          pendingOutputStreamOffsetCount);\n      clearRenderedFirstFrame();\n    }\n  }\n\n  /**\n   * Returns whether the buffer being processed should be dropped.\n   *\n   * @param earlyUs The time until the buffer should be presented in microseconds. A negative value\n   *     indicates that the buffer is late.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   * @param isLastBuffer Whether the buffer is the last buffer in the current stream.\n   */\n  protected boolean shouldDropOutputBuffer(\n      long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {\n    return isBufferLate(earlyUs) && !isLastBuffer;\n  }\n\n  /**\n   * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after\n   * the current playback position, if possible.\n   *\n   * @param earlyUs The time until the current buffer should be presented in microseconds. A\n   *     negative value indicates that the buffer is late.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   * @param isLastBuffer Whether the buffer is the last buffer in the current stream.\n   */\n  protected boolean shouldDropBuffersToKeyframe(\n      long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {\n    return isBufferVeryLate(earlyUs) && !isLastBuffer;\n  }\n\n  /**\n   * Returns whether to force rendering an output buffer.\n   *\n   * @param earlyUs The time until the current buffer should be presented in microseconds. A\n   *     negative value indicates that the buffer is late.\n   * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in\n   *     microseconds.\n   * @return Returns whether to force rendering an output buffer.\n   */\n  protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {\n    // Force render late buffers every 100ms to avoid frozen video effect.\n    return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;\n  }\n\n  /**\n   * Skips the output buffer with the specified index.\n   *\n   * @param codec The codec that owns the output buffer.\n   * @param index The index of the output buffer to skip.\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   */\n  protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {\n    TraceUtil.beginSection(\"skipVideoBuffer\");\n    codec.releaseOutputBuffer(index, false);\n    TraceUtil.endSection();\n    decoderCounters.skippedOutputBufferCount++;\n  }\n\n  /**\n   * Drops the output buffer with the specified index.\n   *\n   * @param codec The codec that owns the output buffer.\n   * @param index The index of the output buffer to drop.\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   */\n  protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {\n    TraceUtil.beginSection(\"dropVideoBuffer\");\n    codec.releaseOutputBuffer(index, false);\n    TraceUtil.endSection();\n    updateDroppedBufferCounters(1);\n  }\n\n  /**\n   * Drops frames from the current output buffer to the next keyframe at or before the playback\n   * position. If no such keyframe exists, as the playback position is inside the same group of\n   * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise.\n   *\n   * @param codec The codec that owns the output buffer.\n   * @param index The index of the output buffer to drop.\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   * @param positionUs The current playback position, in microseconds.\n   * @param treatDroppedBuffersAsSkipped Whether dropped buffers should be treated as intentionally\n   *     skipped.\n   * @return Whether any buffers were dropped.\n   * @throws ExoPlaybackException If an error occurs flushing the codec.\n   */\n  protected boolean maybeDropBuffersToKeyframe(\n      MediaCodec codec,\n      int index,\n      long presentationTimeUs,\n      long positionUs,\n      boolean treatDroppedBuffersAsSkipped)\n      throws ExoPlaybackException {\n    int droppedSourceBufferCount = skipSource(positionUs);\n    if (droppedSourceBufferCount == 0) {\n      return false;\n    }\n    decoderCounters.droppedToKeyframeCount++;\n    // We dropped some buffers to catch up, so update the decoder counters and flush the codec,\n    // which releases all pending buffers buffers including the current output buffer.\n    int totalDroppedBufferCount = buffersInCodecCount + droppedSourceBufferCount;\n    if (treatDroppedBuffersAsSkipped) {\n      decoderCounters.skippedOutputBufferCount += totalDroppedBufferCount;\n    } else {\n      updateDroppedBufferCounters(totalDroppedBufferCount);\n    }\n    flushOrReinitializeCodec();\n    return true;\n  }\n\n  /**\n   * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were\n   * dropped.\n   *\n   * @param droppedBufferCount The number of additional dropped buffers.\n   */\n  protected void updateDroppedBufferCounters(int droppedBufferCount) {\n    decoderCounters.droppedBufferCount += droppedBufferCount;\n    droppedFrames += droppedBufferCount;\n    consecutiveDroppedFrameCount += droppedBufferCount;\n    decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount,\n        decoderCounters.maxConsecutiveDroppedBufferCount);\n    if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) {\n      maybeNotifyDroppedFrames();\n    }\n  }\n\n  /**\n   * Renders the output buffer with the specified index. This method is only called if the platform\n   * API version of the device is less than 21.\n   *\n   * @param codec The codec that owns the output buffer.\n   * @param index The index of the output buffer to drop.\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   */\n  protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {\n    maybeNotifyVideoSizeChanged();\n    TraceUtil.beginSection(\"releaseOutputBuffer\");\n    codec.releaseOutputBuffer(index, true);\n    TraceUtil.endSection();\n    lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;\n    decoderCounters.renderedOutputBufferCount++;\n    consecutiveDroppedFrameCount = 0;\n    maybeNotifyRenderedFirstFrame();\n  }\n\n  /**\n   * Renders the output buffer with the specified index. This method is only called if the platform\n   * API version of the device is 21 or later.\n   *\n   * @param codec The codec that owns the output buffer.\n   * @param index The index of the output buffer to drop.\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.\n   */\n  @TargetApi(21)\n  protected void renderOutputBufferV21(\n      MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {\n    maybeNotifyVideoSizeChanged();\n    TraceUtil.beginSection(\"releaseOutputBuffer\");\n    codec.releaseOutputBuffer(index, releaseTimeNs);\n    TraceUtil.endSection();\n    lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;\n    decoderCounters.renderedOutputBufferCount++;\n    consecutiveDroppedFrameCount = 0;\n    maybeNotifyRenderedFirstFrame();\n  }\n\n  private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) {\n    return Util.SDK_INT >= 23\n        && !tunneling\n        && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)\n        && (!codecInfo.secure || DummySurface.isSecureSupported(context));\n  }\n\n  private void setJoiningDeadlineMs() {\n    joiningDeadlineMs = allowedJoiningTimeMs > 0\n        ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;\n  }\n\n  private void clearRenderedFirstFrame() {\n    renderedFirstFrame = false;\n    // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for\n    // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and\n    // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and\n    // above.\n    if (Util.SDK_INT >= 23 && tunneling) {\n      MediaCodec codec = getCodec();\n      // If codec is null then the listener will be instantiated in configureCodec.\n      if (codec != null) {\n        tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);\n      }\n    }\n  }\n\n  /* package */ void maybeNotifyRenderedFirstFrame() {\n    if (!renderedFirstFrame) {\n      renderedFirstFrame = true;\n      eventDispatcher.renderedFirstFrame(surface);\n    }\n  }\n\n  private void maybeRenotifyRenderedFirstFrame() {\n    if (renderedFirstFrame) {\n      eventDispatcher.renderedFirstFrame(surface);\n    }\n  }\n\n  private void clearReportedVideoSize() {\n    reportedWidth = Format.NO_VALUE;\n    reportedHeight = Format.NO_VALUE;\n    reportedPixelWidthHeightRatio = Format.NO_VALUE;\n    reportedUnappliedRotationDegrees = Format.NO_VALUE;\n  }\n\n  private void maybeNotifyVideoSizeChanged() {\n    if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE)\n      && (reportedWidth != currentWidth || reportedHeight != currentHeight\n        || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees\n        || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) {\n      eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,\n          currentPixelWidthHeightRatio);\n      reportedWidth = currentWidth;\n      reportedHeight = currentHeight;\n      reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;\n      reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;\n    }\n  }\n\n  private void maybeRenotifyVideoSizeChanged() {\n    if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {\n      eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight,\n          reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio);\n    }\n  }\n\n  private void maybeNotifyDroppedFrames() {\n    if (droppedFrames > 0) {\n      long now = SystemClock.elapsedRealtime();\n      long elapsedMs = now - droppedFrameAccumulationStartTimeMs;\n      eventDispatcher.droppedFrames(droppedFrames, elapsedMs);\n      droppedFrames = 0;\n      droppedFrameAccumulationStartTimeMs = now;\n    }\n  }\n\n  private static boolean isBufferLate(long earlyUs) {\n    // Class a buffer as late if it should have been presented more than 30 ms ago.\n    return earlyUs < -30000;\n  }\n\n  private static boolean isBufferVeryLate(long earlyUs) {\n    // Class a buffer as very late if it should have been presented more than 500 ms ago.\n    return earlyUs < -500000;\n  }\n\n  @TargetApi(29)\n  private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) {\n    Bundle codecParameters = new Bundle();\n    codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo);\n    codec.setParameters(codecParameters);\n  }\n\n  @TargetApi(23)\n  private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) {\n    codec.setOutputSurface(surface);\n  }\n\n  @TargetApi(21)\n  private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) {\n    mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true);\n    mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId);\n  }\n\n  /**\n   * Returns the framework {@link MediaFormat} that should be used to configure the decoder.\n   *\n   * @param format The {@link Format} of media.\n   * @param codecMimeType The MIME type handled by the codec.\n   * @param codecMaxValues Codec max values that should be used when configuring the decoder.\n   * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if\n   *     no codec operating rate should be set.\n   * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by\n   *     default that isn't compatible with ExoPlayer.\n   * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link\n   *     C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.\n   * @return The framework {@link MediaFormat} that should be used to configure the decoder.\n   */\n  @SuppressLint(\"InlinedApi\")\n  protected MediaFormat getMediaFormat(\n      Format format,\n      String codecMimeType,\n      CodecMaxValues codecMaxValues,\n      float codecOperatingRate,\n      boolean deviceNeedsNoPostProcessWorkaround,\n      int tunnelingAudioSessionId) {\n    MediaFormat mediaFormat = new MediaFormat();\n    // Set format parameters that should always be set.\n    mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);\n    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width);\n    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height);\n    MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);\n    // Set format parameters that may be unset.\n    MediaFormatUtil.maybeSetFloat(mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate);\n    MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);\n    MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo);\n    if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) {\n      // Some phones require the profile to be set on the codec.\n      // See https://github.com/google/ExoPlayer/pull/5438.\n      Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);\n      if (codecProfileAndLevel != null) {\n        MediaFormatUtil.maybeSetInteger(\n            mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first);\n      }\n    }\n    // Set codec max values.\n    mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width);\n    mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height);\n    MediaFormatUtil.maybeSetInteger(\n        mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize);\n    // Set codec configuration values.\n    if (Util.SDK_INT >= 23) {\n      mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);\n      if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) {\n        mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);\n      }\n    }\n    if (deviceNeedsNoPostProcessWorkaround) {\n      mediaFormat.setInteger(\"no-post-process\", 1);\n      mediaFormat.setInteger(\"auto-frc\", 0);\n    }\n    if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {\n      configureTunnelingV21(mediaFormat, tunnelingAudioSessionId);\n    }\n    return mediaFormat;\n  }\n\n  /**\n   * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way\n   * that will allow possible adaptation to other compatible formats in {@code streamFormats}.\n   *\n   * @param codecInfo Information about the {@link MediaCodec} being configured.\n   * @param format The {@link Format} for which the codec is being configured.\n   * @param streamFormats The possible stream formats.\n   * @return Suitable {@link CodecMaxValues}.\n   */\n  protected CodecMaxValues getCodecMaxValues(\n      MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {\n    int maxWidth = format.width;\n    int maxHeight = format.height;\n    int maxInputSize = getMaxInputSize(codecInfo, format);\n    if (streamFormats.length == 1) {\n      // The single entry in streamFormats must correspond to the format for which the codec is\n      // being configured.\n      if (maxInputSize != Format.NO_VALUE) {\n        int codecMaxInputSize =\n            getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height);\n        if (codecMaxInputSize != Format.NO_VALUE) {\n          // Scale up the initial video decoder maximum input size so playlist item transitions with\n          // small increases in maximum sample size don't require reinitialization. This only makes\n          // a difference if the exact maximum sample sizes are known from the container.\n          int scaledMaxInputSize =\n              (int) (maxInputSize * INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR);\n          // Avoid exceeding the maximum expected for the codec.\n          maxInputSize = Math.min(scaledMaxInputSize, codecMaxInputSize);\n        }\n      }\n      return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);\n    }\n    boolean haveUnknownDimensions = false;\n    for (Format streamFormat : streamFormats) {\n      if (codecInfo.isSeamlessAdaptationSupported(\n          format, streamFormat, /* isNewFormatComplete= */ false)) {\n        haveUnknownDimensions |=\n            (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE);\n        maxWidth = Math.max(maxWidth, streamFormat.width);\n        maxHeight = Math.max(maxHeight, streamFormat.height);\n        maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat));\n      }\n    }\n    if (haveUnknownDimensions) {\n      Log.w(TAG, \"Resolutions unknown. Codec max resolution: \" + maxWidth + \"x\" + maxHeight);\n      Point codecMaxSize = getCodecMaxSize(codecInfo, format);\n      if (codecMaxSize != null) {\n        maxWidth = Math.max(maxWidth, codecMaxSize.x);\n        maxHeight = Math.max(maxHeight, codecMaxSize.y);\n        maxInputSize =\n            Math.max(\n                maxInputSize,\n                getCodecMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight));\n        Log.w(TAG, \"Codec max resolution adjusted to: \" + maxWidth + \"x\" + maxHeight);\n      }\n    }\n    return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);\n  }\n\n  @Override\n  protected DecoderException createDecoderException(\n      Throwable cause, @Nullable MediaCodecInfo codecInfo) {\n    return new VideoDecoderException(cause, codecInfo, surface);\n  }\n\n  /**\n   * Returns a maximum video size to use when configuring a codec for {@code format} in a way that\n   * will allow possible adaptation to other compatible formats that are expected to have the same\n   * aspect ratio, but whose sizes are unknown.\n   *\n   * @param codecInfo Information about the {@link MediaCodec} being configured.\n   * @param format The {@link Format} for which the codec is being configured.\n   * @return The maximum video size to use, or null if the size of {@code format} should be used.\n   */\n  private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {\n    boolean isVerticalVideo = format.height > format.width;\n    int formatLongEdgePx = isVerticalVideo ? format.height : format.width;\n    int formatShortEdgePx = isVerticalVideo ? format.width : format.height;\n    float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx;\n    for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) {\n      int shortEdgePx = (int) (longEdgePx * aspectRatio);\n      if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) {\n        // Don't return a size not larger than the format for which the codec is being configured.\n        return null;\n      } else if (Util.SDK_INT >= 21) {\n        Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx,\n            isVerticalVideo ? longEdgePx : shortEdgePx);\n        float frameRate = format.frameRate;\n        if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) {\n          return alignedSize;\n        }\n      } else {\n        try {\n          // Conservatively assume the codec requires 16px width and height alignment.\n          longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;\n          shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;\n          if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {\n            return new Point(\n                isVerticalVideo ? shortEdgePx : longEdgePx,\n                isVerticalVideo ? longEdgePx : shortEdgePx);\n          }\n        } catch (DecoderQueryException e) {\n          // We tried our best. Give up!\n          return null;\n        }\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Returns a maximum input buffer size for a given {@link MediaCodec} and {@link Format}.\n   *\n   * @param codecInfo Information about the {@link MediaCodec} being configured.\n   * @param format The format.\n   * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not\n   *     be determined.\n   */\n  private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) {\n    if (format.maxInputSize != Format.NO_VALUE) {\n      // The format defines an explicit maximum input size. Add the total size of initialization\n      // data buffers, as they may need to be queued in the same input buffer as the largest sample.\n      int totalInitializationDataSize = 0;\n      int initializationDataCount = format.initializationData.size();\n      for (int i = 0; i < initializationDataCount; i++) {\n        totalInitializationDataSize += format.initializationData.get(i).length;\n      }\n      return format.maxInputSize + totalInitializationDataSize;\n    } else {\n      // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of\n      // initialization data.\n      return getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height);\n    }\n  }\n\n  /**\n   * Returns a maximum input size for a given codec, MIME type, width and height.\n   *\n   * @param codecInfo Information about the {@link MediaCodec} being configured.\n   * @param sampleMimeType The format mime type.\n   * @param width The width in pixels.\n   * @param height The height in pixels.\n   * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be\n   *     determined.\n   */\n  private static int getCodecMaxInputSize(\n      MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) {\n    if (width == Format.NO_VALUE || height == Format.NO_VALUE) {\n      // We can't infer a maximum input size without video dimensions.\n      return Format.NO_VALUE;\n    }\n\n    // Attempt to infer a maximum input size from the format.\n    int maxPixels;\n    int minCompressionRatio;\n    switch (sampleMimeType) {\n      case MimeTypes.VIDEO_H263:\n      case MimeTypes.VIDEO_MP4V:\n        maxPixels = width * height;\n        minCompressionRatio = 2;\n        break;\n      case MimeTypes.VIDEO_H264:\n        if (\"BRAVIA 4K 2015\".equals(Util.MODEL) // Sony Bravia 4K\n            || (\"Amazon\".equals(Util.MANUFACTURER)\n                && (\"KFSOWI\".equals(Util.MODEL) // Kindle Soho\n                    || (\"AFTS\".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2\n          // Use the default value for cases where platform limitations may prevent buffers of the\n          // calculated maximum input size from being allocated.\n          return Format.NO_VALUE;\n        }\n        // Round up width/height to an integer number of macroblocks.\n        maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16;\n        minCompressionRatio = 2;\n        break;\n      case MimeTypes.VIDEO_VP8:\n        // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.\n        maxPixels = width * height;\n        minCompressionRatio = 2;\n        break;\n      case MimeTypes.VIDEO_H265:\n      case MimeTypes.VIDEO_VP9:\n        maxPixels = width * height;\n        minCompressionRatio = 4;\n        break;\n      default:\n        // Leave the default max input size.\n        return Format.NO_VALUE;\n    }\n    // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.\n    return (maxPixels * 3) / (2 * minCompressionRatio);\n  }\n\n  /**\n   * Returns whether the device is known to do post processing by default that isn't compatible with\n   * ExoPlayer.\n   *\n   * @return Whether the device is known to do post processing by default that isn't compatible with\n   *     ExoPlayer.\n   */\n  private static boolean deviceNeedsNoPostProcessWorkaround() {\n    // Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of\n    // content to the refresh rate of the display. For example playback of 23.976fps content is\n    // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the\n    // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions\n    // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing\n    // operations that can modify frame output timestamps, which is incompatible with ExoPlayer's\n    // logic for skipping decode-only frames.\n    return \"NVIDIA\".equals(Util.MANUFACTURER);\n  }\n\n  /*\n   * TODO:\n   *\n   * 1. Validate that Android device certification now ensures correct behavior, and add a\n   *    corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26).\n   * 2. Determine a complete list of affected devices.\n   * 3. Some of the devices in this list only fail to support setOutputSurface when switching from\n   *    a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface),\n   *    and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have\n   *    different pixel formats. If we can find a way to query the Surface instances to determine\n   *    whether this case applies, then we'll be able to provide a more targeted workaround.\n   */\n  /**\n   * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)}\n   * incorrectly.\n   *\n   * <p>If true is returned then we fall back to releasing and re-instantiating the codec instead.\n   *\n   * @param name The name of the codec.\n   * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)}\n   *     incorrectly.\n   */\n  protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {\n    if (name.startsWith(\"OMX.google\")) {\n      // Google OMX decoders are not known to have this issue on any API level.\n      return false;\n    }\n    synchronized (MediaCodecVideoRenderer.class) {\n      if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {\n        if (Util.SDK_INT <= 27 && (\"dangal\".equals(Util.DEVICE) || \"HWEML\".equals(Util.DEVICE))) {\n          // A small number of devices are affected on API level 27:\n          // https://github.com/google/ExoPlayer/issues/5169.\n          deviceNeedsSetOutputSurfaceWorkaround = true;\n        } else if (Util.SDK_INT >= 27) {\n          // In general, devices running API level 27 or later should be unaffected. Do nothing.\n        } else {\n          // Enable the workaround on a per-device basis. Works around:\n          // https://github.com/google/ExoPlayer/issues/3236,\n          // https://github.com/google/ExoPlayer/issues/3355,\n          // https://github.com/google/ExoPlayer/issues/3439,\n          // https://github.com/google/ExoPlayer/issues/3724,\n          // https://github.com/google/ExoPlayer/issues/3835,\n          // https://github.com/google/ExoPlayer/issues/4006,\n          // https://github.com/google/ExoPlayer/issues/4084,\n          // https://github.com/google/ExoPlayer/issues/4104,\n          // https://github.com/google/ExoPlayer/issues/4134,\n          // https://github.com/google/ExoPlayer/issues/4315,\n          // https://github.com/google/ExoPlayer/issues/4419,\n          // https://github.com/google/ExoPlayer/issues/4460,\n          // https://github.com/google/ExoPlayer/issues/4468,\n          // https://github.com/google/ExoPlayer/issues/5312,\n          // https://github.com/google/ExoPlayer/issues/6503.\n          switch (Util.DEVICE) {\n            case \"1601\":\n            case \"1713\":\n            case \"1714\":\n            case \"A10-70F\":\n            case \"A10-70L\":\n            case \"A1601\":\n            case \"A2016a40\":\n            case \"A7000-a\":\n            case \"A7000plus\":\n            case \"A7010a48\":\n            case \"A7020a48\":\n            case \"AquaPowerM\":\n            case \"ASUS_X00AD_2\":\n            case \"Aura_Note_2\":\n            case \"BLACK-1X\":\n            case \"BRAVIA_ATV2\":\n            case \"BRAVIA_ATV3_4K\":\n            case \"C1\":\n            case \"ComioS1\":\n            case \"CP8676_I02\":\n            case \"CPH1609\":\n            case \"CPY83_I00\":\n            case \"cv1\":\n            case \"cv3\":\n            case \"deb\":\n            case \"E5643\":\n            case \"ELUGA_A3_Pro\":\n            case \"ELUGA_Note\":\n            case \"ELUGA_Prim\":\n            case \"ELUGA_Ray_X\":\n            case \"EverStar_S\":\n            case \"F3111\":\n            case \"F3113\":\n            case \"F3116\":\n            case \"F3211\":\n            case \"F3213\":\n            case \"F3215\":\n            case \"F3311\":\n            case \"flo\":\n            case \"fugu\":\n            case \"GiONEE_CBL7513\":\n            case \"GiONEE_GBL7319\":\n            case \"GIONEE_GBL7360\":\n            case \"GIONEE_SWW1609\":\n            case \"GIONEE_SWW1627\":\n            case \"GIONEE_SWW1631\":\n            case \"GIONEE_WBL5708\":\n            case \"GIONEE_WBL7365\":\n            case \"GIONEE_WBL7519\":\n            case \"griffin\":\n            case \"htc_e56ml_dtul\":\n            case \"hwALE-H\":\n            case \"HWBLN-H\":\n            case \"HWCAM-H\":\n            case \"HWVNS-H\":\n            case \"HWWAS-H\":\n            case \"i9031\":\n            case \"iball8735_9806\":\n            case \"Infinix-X572\":\n            case \"iris60\":\n            case \"itel_S41\":\n            case \"j2xlteins\":\n            case \"JGZ\":\n            case \"K50a40\":\n            case \"kate\":\n            case \"l5460\":\n            case \"le_x6\":\n            case \"LS-5017\":\n            case \"M5c\":\n            case \"manning\":\n            case \"marino_f\":\n            case \"MEIZU_M5\":\n            case \"mh\":\n            case \"mido\":\n            case \"MX6\":\n            case \"namath\":\n            case \"nicklaus_f\":\n            case \"NX541J\":\n            case \"NX573J\":\n            case \"OnePlus5T\":\n            case \"p212\":\n            case \"P681\":\n            case \"P85\":\n            case \"panell_d\":\n            case \"panell_dl\":\n            case \"panell_ds\":\n            case \"panell_dt\":\n            case \"PB2-670M\":\n            case \"PGN528\":\n            case \"PGN610\":\n            case \"PGN611\":\n            case \"Phantom6\":\n            case \"Pixi4-7_3G\":\n            case \"Pixi5-10_4G\":\n            case \"PLE\":\n            case \"PRO7S\":\n            case \"Q350\":\n            case \"Q4260\":\n            case \"Q427\":\n            case \"Q4310\":\n            case \"Q5\":\n            case \"QM16XE_U\":\n            case \"QX1\":\n            case \"santoni\":\n            case \"Slate_Pro\":\n            case \"SVP-DTV15\":\n            case \"s905x018\":\n            case \"taido_row\":\n            case \"TB3-730F\":\n            case \"TB3-730X\":\n            case \"TB3-850F\":\n            case \"TB3-850M\":\n            case \"tcl_eu\":\n            case \"V1\":\n            case \"V23GB\":\n            case \"V5\":\n            case \"vernee_M5\":\n            case \"watson\":\n            case \"whyred\":\n            case \"woods_f\":\n            case \"woods_fn\":\n            case \"X3_HK\":\n            case \"XE2X\":\n            case \"XT1663\":\n            case \"Z12_PRO\":\n            case \"Z80\":\n              deviceNeedsSetOutputSurfaceWorkaround = true;\n              break;\n            default:\n              // Do nothing.\n              break;\n          }\n          switch (Util.MODEL) {\n            case \"AFTA\":\n            case \"AFTN\":\n            case \"JSN-L21\":\n              deviceNeedsSetOutputSurfaceWorkaround = true;\n              break;\n            default:\n              // Do nothing.\n              break;\n          }\n        }\n        evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;\n      }\n    }\n    return deviceNeedsSetOutputSurfaceWorkaround;\n  }\n\n  protected Surface getSurface() {\n    return surface;\n  }\n\n  protected static final class CodecMaxValues {\n\n    public final int width;\n    public final int height;\n    public final int inputSize;\n\n    public CodecMaxValues(int width, int height, int inputSize) {\n      this.width = width;\n      this.height = height;\n      this.inputSize = inputSize;\n    }\n\n  }\n\n  @TargetApi(23)\n  private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener {\n\n    private OnFrameRenderedListenerV23(MediaCodec codec) {\n      codec.setOnFrameRenderedListener(this, new Handler());\n    }\n\n    @Override\n    public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) {\n      if (this != tunnelingOnFrameRenderedListener) {\n        // Stale event.\n        return;\n      }\n      if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) {\n        onProcessedTunneledEndOfStream();\n      } else {\n        onProcessedTunneledBuffer(presentationTimeUs);\n      }\n    }\n\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.view.Surface;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.decoder.SimpleDecoder;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.TimedValueQueue;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** Decodes and renders video using a {@link SimpleDecoder}. */\npublic abstract class SimpleDecoderVideoRenderer extends BaseRenderer {\n\n  /** Decoder reinitialization states. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    REINITIALIZATION_STATE_NONE,\n    REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,\n    REINITIALIZATION_STATE_WAIT_END_OF_STREAM\n  })\n  private @interface ReinitializationState {}\n  /** The decoder does not need to be re-initialized. */\n  private static final int REINITIALIZATION_STATE_NONE = 0;\n  /**\n   * The input format has changed in a way that requires the decoder to be re-initialized, but we\n   * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to\n   * ensure that it outputs any remaining buffers before we release it.\n   */\n  private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;\n  /**\n   * The input format has changed in a way that requires the decoder to be re-initialized, and we've\n   * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an\n   * end of stream signal to indicate that it has output any remaining buffers before we release it.\n   */\n  private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;\n\n  private final long allowedJoiningTimeMs;\n  private final int maxDroppedFramesToNotify;\n  private final boolean playClearSamplesWithoutKeys;\n  private final EventDispatcher eventDispatcher;\n  private final TimedValueQueue<Format> formatQueue;\n  private final DecoderInputBuffer flagsOnlyBuffer;\n  private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;\n\n  private Format inputFormat;\n  private Format outputFormat;\n  private SimpleDecoder<\n          VideoDecoderInputBuffer,\n          ? extends VideoDecoderOutputBuffer,\n          ? extends VideoDecoderException>\n      decoder;\n  private VideoDecoderInputBuffer inputBuffer;\n  private VideoDecoderOutputBuffer outputBuffer;\n  @Nullable private Surface surface;\n  @Nullable private VideoDecoderOutputBufferRenderer outputBufferRenderer;\n  @C.VideoOutputMode private int outputMode;\n\n  @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;\n  @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;\n\n  @ReinitializationState private int decoderReinitializationState;\n  private boolean decoderReceivedBuffers;\n\n  private boolean renderedFirstFrame;\n  private long initialPositionUs;\n  private long joiningDeadlineMs;\n  private boolean waitingForKeys;\n  private boolean waitingForFirstSampleInFormat;\n\n  private boolean inputStreamEnded;\n  private boolean outputStreamEnded;\n  private int reportedWidth;\n  private int reportedHeight;\n\n  private long droppedFrameAccumulationStartTimeMs;\n  private int droppedFrames;\n  private int consecutiveDroppedFrameCount;\n  private int buffersInCodecCount;\n  private long lastRenderTimeUs;\n  private long outputStreamOffsetUs;\n\n  /** Decoder event counters used for debugging purposes. */\n  protected DecoderCounters decoderCounters;\n\n  /**\n   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer\n   *     can attempt to seamlessly join an ongoing playback.\n   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be\n   *     null if delivery of events is not required.\n   * @param eventListener A listener of events. May be null if delivery of events is not required.\n   * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between\n   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.\n   * @param drmSessionManager For use with encrypted media. May be null if support for encrypted\n   *     media is not required.\n   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.\n   *     For example a media file may start with a short clear region so as to allow playback to\n   *     begin in parallel with key acquisition. This parameter specifies whether the renderer is\n   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}\n   *     has obtained the keys necessary to decrypt encrypted regions of the media.\n   */\n  protected SimpleDecoderVideoRenderer(\n      long allowedJoiningTimeMs,\n      @Nullable Handler eventHandler,\n      @Nullable VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify,\n      @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys) {\n    super(C.TRACK_TYPE_VIDEO);\n    this.allowedJoiningTimeMs = allowedJoiningTimeMs;\n    this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;\n    this.drmSessionManager = drmSessionManager;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    joiningDeadlineMs = C.TIME_UNSET;\n    clearReportedVideoSize();\n    formatQueue = new TimedValueQueue<>();\n    flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();\n    eventDispatcher = new EventDispatcher(eventHandler, eventListener);\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n    outputMode = C.VIDEO_OUTPUT_MODE_NONE;\n  }\n\n  // BaseRenderer implementation.\n\n  @Override\n  @Capabilities\n  public final int supportsFormat(Format format) {\n    return supportsFormatInternal(drmSessionManager, format);\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (outputStreamEnded) {\n      return;\n    }\n\n    if (inputFormat == null) {\n      // We don't have a format yet, so try and read one.\n      FormatHolder formatHolder = getFormatHolder();\n      flagsOnlyBuffer.clear();\n      int result = readSource(formatHolder, flagsOnlyBuffer, true);\n      if (result == C.RESULT_FORMAT_READ) {\n        onInputFormatChanged(formatHolder);\n      } else if (result == C.RESULT_BUFFER_READ) {\n        // End of stream read having not read a format.\n        Assertions.checkState(flagsOnlyBuffer.isEndOfStream());\n        inputStreamEnded = true;\n        outputStreamEnded = true;\n        return;\n      } else {\n        // We still don't have a format and can't make progress without one.\n        return;\n      }\n    }\n\n    // If we don't have a decoder yet, we need to instantiate one.\n    maybeInitDecoder();\n\n    if (decoder != null) {\n      try {\n        // Rendering loop.\n        TraceUtil.beginSection(\"drainAndFeed\");\n        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}\n        while (feedInputBuffer()) {}\n        TraceUtil.endSection();\n      } catch (VideoDecoderException e) {\n        throw createRendererException(e, inputFormat);\n      }\n      decoderCounters.ensureUpdated();\n    }\n  }\n\n  @Override\n  public boolean isEnded() {\n    return outputStreamEnded;\n  }\n\n  @Override\n  public boolean isReady() {\n    if (waitingForKeys) {\n      return false;\n    }\n    if (inputFormat != null\n        && (isSourceReady() || outputBuffer != null)\n        && (renderedFirstFrame || !hasOutput())) {\n      // Ready. If we were joining then we've now joined, so clear the joining deadline.\n      joiningDeadlineMs = C.TIME_UNSET;\n      return true;\n    } else if (joiningDeadlineMs == C.TIME_UNSET) {\n      // Not joining.\n      return false;\n    } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) {\n      // Joining and still within the joining deadline.\n      return true;\n    } else {\n      // The joining deadline has been exceeded. Give up and clear the deadline.\n      joiningDeadlineMs = C.TIME_UNSET;\n      return false;\n    }\n  }\n\n  // Protected methods.\n\n  @Override\n  protected void onEnabled(boolean joining) throws ExoPlaybackException {\n    decoderCounters = new DecoderCounters();\n    eventDispatcher.enabled(decoderCounters);\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    inputStreamEnded = false;\n    outputStreamEnded = false;\n    clearRenderedFirstFrame();\n    initialPositionUs = C.TIME_UNSET;\n    consecutiveDroppedFrameCount = 0;\n    if (decoder != null) {\n      flushDecoder();\n    }\n    if (joining) {\n      setJoiningDeadlineMs();\n    } else {\n      joiningDeadlineMs = C.TIME_UNSET;\n    }\n    formatQueue.clear();\n  }\n\n  @Override\n  protected void onStarted() {\n    droppedFrames = 0;\n    droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();\n    lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;\n  }\n\n  @Override\n  protected void onStopped() {\n    joiningDeadlineMs = C.TIME_UNSET;\n    maybeNotifyDroppedFrames();\n  }\n\n  @Override\n  protected void onDisabled() {\n    inputFormat = null;\n    waitingForKeys = false;\n    clearReportedVideoSize();\n    clearRenderedFirstFrame();\n    try {\n      setSourceDrmSession(null);\n      releaseDecoder();\n    } finally {\n      eventDispatcher.disabled(decoderCounters);\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\n    outputStreamOffsetUs = offsetUs;\n    super.onStreamChanged(formats, offsetUs);\n  }\n\n  /**\n   * Called when a decoder has been created and configured.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param name The name of the decoder that was initialized.\n   * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization\n   *     finished.\n   * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds.\n   */\n  @CallSuper\n  protected void onDecoderInitialized(\n      String name, long initializedTimestampMs, long initializationDurationMs) {\n    eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);\n  }\n\n  /**\n   * Flushes the decoder.\n   *\n   * @throws ExoPlaybackException If an error occurs reinitializing a decoder.\n   */\n  @CallSuper\n  protected void flushDecoder() throws ExoPlaybackException {\n    waitingForKeys = false;\n    buffersInCodecCount = 0;\n    if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {\n      releaseDecoder();\n      maybeInitDecoder();\n    } else {\n      inputBuffer = null;\n      if (outputBuffer != null) {\n        outputBuffer.release();\n        outputBuffer = null;\n      }\n      decoder.flush();\n      decoderReceivedBuffers = false;\n    }\n  }\n\n  /** Releases the decoder. */\n  @CallSuper\n  protected void releaseDecoder() {\n    inputBuffer = null;\n    outputBuffer = null;\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n    decoderReceivedBuffers = false;\n    buffersInCodecCount = 0;\n    if (decoder != null) {\n      decoder.release();\n      decoder = null;\n      decoderCounters.decoderReleaseCount++;\n    }\n    setDecoderDrmSession(null);\n  }\n\n  /**\n   * Called when a new format is read from the upstream source.\n   *\n   * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}.\n   * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.\n   */\n  @CallSuper\n  @SuppressWarnings(\"unchecked\")\n  protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {\n    waitingForFirstSampleInFormat = true;\n    Format newFormat = Assertions.checkNotNull(formatHolder.format);\n    if (formatHolder.includesDrmSession) {\n      setSourceDrmSession((DrmSession<ExoMediaCrypto>) formatHolder.drmSession);\n    } else {\n      sourceDrmSession =\n          getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);\n    }\n    inputFormat = newFormat;\n\n    if (sourceDrmSession != decoderDrmSession) {\n      if (decoderReceivedBuffers) {\n        // Signal end of stream and wait for any final output buffers before re-initialization.\n        decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;\n      } else {\n        // There aren't any final output buffers, so release the decoder immediately.\n        releaseDecoder();\n        maybeInitDecoder();\n      }\n    }\n\n    eventDispatcher.inputFormatChanged(inputFormat);\n  }\n\n  /**\n   * Called immediately before an input buffer is queued into the decoder.\n   *\n   * <p>The default implementation is a no-op.\n   *\n   * @param buffer The buffer that will be queued.\n   */\n  protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) {\n    // Do nothing.\n  }\n\n  /**\n   * Called when an output buffer is successfully processed.\n   *\n   * @param presentationTimeUs The timestamp associated with the output buffer.\n   */\n  @CallSuper\n  protected void onProcessedOutputBuffer(long presentationTimeUs) {\n    buffersInCodecCount--;\n  }\n\n  /**\n   * Returns whether the buffer being processed should be dropped.\n   *\n   * @param earlyUs The time until the buffer should be presented in microseconds. A negative value\n   *     indicates that the buffer is late.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   */\n  protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {\n    return isBufferLate(earlyUs);\n  }\n\n  /**\n   * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after\n   * the current playback position, if possible.\n   *\n   * @param earlyUs The time until the current buffer should be presented in microseconds. A\n   *     negative value indicates that the buffer is late.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   */\n  protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) {\n    return isBufferVeryLate(earlyUs);\n  }\n\n  /**\n   * Returns whether to force rendering an output buffer.\n   *\n   * @param earlyUs The time until the current buffer should be presented in microseconds. A\n   *     negative value indicates that the buffer is late.\n   * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in\n   *     microseconds.\n   * @return Returns whether to force rendering an output buffer.\n   */\n  protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {\n    return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000;\n  }\n\n  /**\n   * Skips the specified output buffer and releases it.\n   *\n   * @param outputBuffer The output buffer to skip.\n   */\n  protected void skipOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {\n    decoderCounters.skippedOutputBufferCount++;\n    outputBuffer.release();\n  }\n\n  /**\n   * Drops the specified output buffer and releases it.\n   *\n   * @param outputBuffer The output buffer to drop.\n   */\n  protected void dropOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {\n    updateDroppedBufferCounters(1);\n    outputBuffer.release();\n  }\n\n  /**\n   * Drops frames from the current output buffer to the next keyframe at or before the playback\n   * position. If no such keyframe exists, as the playback position is inside the same group of\n   * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise.\n   *\n   * @param positionUs The current playback position, in microseconds.\n   * @return Whether any buffers were dropped.\n   * @throws ExoPlaybackException If an error occurs flushing the decoder.\n   */\n  protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException {\n    int droppedSourceBufferCount = skipSource(positionUs);\n    if (droppedSourceBufferCount == 0) {\n      return false;\n    }\n    decoderCounters.droppedToKeyframeCount++;\n    // We dropped some buffers to catch up, so update the decoder counters and flush the decoder,\n    // which releases all pending buffers buffers including the current output buffer.\n    updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);\n    flushDecoder();\n    return true;\n  }\n\n  /**\n   * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were\n   * dropped.\n   *\n   * @param droppedBufferCount The number of additional dropped buffers.\n   */\n  protected void updateDroppedBufferCounters(int droppedBufferCount) {\n    decoderCounters.droppedBufferCount += droppedBufferCount;\n    droppedFrames += droppedBufferCount;\n    consecutiveDroppedFrameCount += droppedBufferCount;\n    decoderCounters.maxConsecutiveDroppedBufferCount =\n        Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount);\n    if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) {\n      maybeNotifyDroppedFrames();\n    }\n  }\n\n  /**\n   * Returns the {@link Capabilities} for the given {@link Format}.\n   *\n   * @param drmSessionManager The renderer's {@link DrmSessionManager}.\n   * @param format The format, which has a video {@link Format#sampleMimeType}.\n   * @return The {@link Capabilities} for this {@link Format}.\n   * @see RendererCapabilities#supportsFormat(Format)\n   */\n  @Capabilities\n  protected abstract int supportsFormatInternal(\n      @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format);\n\n  /**\n   * Creates a decoder for the given format.\n   *\n   * @param format The format for which a decoder is required.\n   * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.\n   *     May be null and can be ignored if decoder does not handle encrypted content.\n   * @return The decoder.\n   * @throws VideoDecoderException If an error occurred creating a suitable decoder.\n   */\n  protected abstract SimpleDecoder<\n          VideoDecoderInputBuffer,\n          ? extends VideoDecoderOutputBuffer,\n          ? extends VideoDecoderException>\n      createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)\n          throws VideoDecoderException;\n\n  /**\n   * Renders the specified output buffer.\n   *\n   * <p>The implementation of this method takes ownership of the output buffer and is responsible\n   * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future.\n   *\n   * @param outputBuffer {@link VideoDecoderOutputBuffer} to render.\n   * @param presentationTimeUs Presentation time in microseconds.\n   * @param outputFormat Output {@link Format}.\n   * @throws VideoDecoderException If an error occurs when rendering the output buffer.\n   */\n  protected void renderOutputBuffer(\n      VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat)\n      throws VideoDecoderException {\n    lastRenderTimeUs = C.msToUs(SystemClock.elapsedRealtime() * 1000);\n    int bufferMode = outputBuffer.mode;\n    boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null;\n    boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null;\n    if (!renderYuv && !renderSurface) {\n      dropOutputBuffer(outputBuffer);\n    } else {\n      maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);\n      if (renderYuv) {\n        outputBufferRenderer.setOutputBuffer(outputBuffer);\n      } else {\n        renderOutputBufferToSurface(outputBuffer, surface);\n      }\n      consecutiveDroppedFrameCount = 0;\n      decoderCounters.renderedOutputBufferCount++;\n      maybeNotifyRenderedFirstFrame();\n    }\n  }\n\n  /**\n   * Renders the specified output buffer to the passed surface.\n   *\n   * <p>The implementation of this method takes ownership of the output buffer and is responsible\n   * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future.\n   *\n   * @param outputBuffer {@link VideoDecoderOutputBuffer} to render.\n   * @param surface Output {@link Surface}.\n   * @throws VideoDecoderException If an error occurs when rendering the output buffer.\n   */\n  protected abstract void renderOutputBufferToSurface(\n      VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VideoDecoderException;\n\n  /**\n   * Sets output surface.\n   *\n   * @param surface Surface.\n   */\n  protected final void setOutputSurface(@Nullable Surface surface) {\n    if (this.surface != surface) {\n      // The output has changed.\n      this.surface = surface;\n      if (surface != null) {\n        outputBufferRenderer = null;\n        outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV;\n        if (decoder != null) {\n          setDecoderOutputMode(outputMode);\n        }\n        onOutputChanged();\n      } else {\n        // The output has been removed. We leave the outputMode of the underlying decoder unchanged\n        // in anticipation that a subsequent output will likely be of the same type.\n        outputMode = C.VIDEO_OUTPUT_MODE_NONE;\n        onOutputRemoved();\n      }\n    } else if (surface != null) {\n      // The output is unchanged and non-null.\n      onOutputReset();\n    }\n  }\n\n  /**\n   * Sets output buffer renderer.\n   *\n   * @param outputBufferRenderer Output buffer renderer.\n   */\n  protected final void setOutputBufferRenderer(\n      @Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer) {\n    if (this.outputBufferRenderer != outputBufferRenderer) {\n      // The output has changed.\n      this.outputBufferRenderer = outputBufferRenderer;\n      if (outputBufferRenderer != null) {\n        surface = null;\n        outputMode = C.VIDEO_OUTPUT_MODE_YUV;\n        if (decoder != null) {\n          setDecoderOutputMode(outputMode);\n        }\n        onOutputChanged();\n      } else {\n        // The output has been removed. We leave the outputMode of the underlying decoder unchanged\n        // in anticipation that a subsequent output will likely be of the same type.\n        outputMode = C.VIDEO_OUTPUT_MODE_NONE;\n        onOutputRemoved();\n      }\n    } else if (outputBufferRenderer != null) {\n      // The output is unchanged and non-null.\n      onOutputReset();\n    }\n  }\n\n  /**\n   * Sets output mode of the decoder.\n   *\n   * @param outputMode Output mode.\n   */\n  protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode);\n\n  // Internal methods.\n\n  private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession.replaceSession(sourceDrmSession, session);\n    sourceDrmSession = session;\n  }\n\n  private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession.replaceSession(decoderDrmSession, session);\n    decoderDrmSession = session;\n  }\n\n  private void maybeInitDecoder() throws ExoPlaybackException {\n    if (decoder != null) {\n      return;\n    }\n\n    setDecoderDrmSession(sourceDrmSession);\n\n    ExoMediaCrypto mediaCrypto = null;\n    if (decoderDrmSession != null) {\n      mediaCrypto = decoderDrmSession.getMediaCrypto();\n      if (mediaCrypto == null) {\n        DrmSessionException drmError = decoderDrmSession.getError();\n        if (drmError != null) {\n          // Continue for now. We may be able to avoid failure if the session recovers, or if a new\n          // input format causes the session to be replaced before it's used.\n        } else {\n          // The drm session isn't open yet.\n          return;\n        }\n      }\n    }\n\n    try {\n      long decoderInitializingTimestamp = SystemClock.elapsedRealtime();\n      decoder = createDecoder(inputFormat, mediaCrypto);\n      setDecoderOutputMode(outputMode);\n      long decoderInitializedTimestamp = SystemClock.elapsedRealtime();\n      onDecoderInitialized(\n          decoder.getName(),\n          decoderInitializedTimestamp,\n          decoderInitializedTimestamp - decoderInitializingTimestamp);\n      decoderCounters.decoderInitCount++;\n    } catch (VideoDecoderException e) {\n      throw createRendererException(e, inputFormat);\n    }\n  }\n\n  private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException {\n    if (decoder == null\n        || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM\n        || inputStreamEnded) {\n      // We need to reinitialize the decoder or the input stream has ended.\n      return false;\n    }\n\n    if (inputBuffer == null) {\n      inputBuffer = decoder.dequeueInputBuffer();\n      if (inputBuffer == null) {\n        return false;\n      }\n    }\n\n    if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {\n      inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n      decoder.queueInputBuffer(inputBuffer);\n      inputBuffer = null;\n      decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;\n      return false;\n    }\n\n    int result;\n    FormatHolder formatHolder = getFormatHolder();\n    if (waitingForKeys) {\n      // We've already read an encrypted sample into buffer, and are waiting for keys.\n      result = C.RESULT_BUFFER_READ;\n    } else {\n      result = readSource(formatHolder, inputBuffer, false);\n    }\n\n    if (result == C.RESULT_NOTHING_READ) {\n      return false;\n    }\n    if (result == C.RESULT_FORMAT_READ) {\n      onInputFormatChanged(formatHolder);\n      return true;\n    }\n    if (inputBuffer.isEndOfStream()) {\n      inputStreamEnded = true;\n      decoder.queueInputBuffer(inputBuffer);\n      inputBuffer = null;\n      return false;\n    }\n    boolean bufferEncrypted = inputBuffer.isEncrypted();\n    waitingForKeys = shouldWaitForKeys(bufferEncrypted);\n    if (waitingForKeys) {\n      return false;\n    }\n    if (waitingForFirstSampleInFormat) {\n      formatQueue.add(inputBuffer.timeUs, inputFormat);\n      waitingForFirstSampleInFormat = false;\n    }\n    inputBuffer.flip();\n    inputBuffer.colorInfo = inputFormat.colorInfo;\n    onQueueInputBuffer(inputBuffer);\n    decoder.queueInputBuffer(inputBuffer);\n    buffersInCodecCount++;\n    decoderReceivedBuffers = true;\n    decoderCounters.inputBufferCount++;\n    inputBuffer = null;\n    return true;\n  }\n\n  /**\n   * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link\n   * #processOutputBuffer(long, long)}.\n   *\n   * @param positionUs The player's current position.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   * @return Whether it may be possible to drain more output data.\n   * @throws ExoPlaybackException If an error occurs draining the output buffer.\n   */\n  private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)\n      throws ExoPlaybackException, VideoDecoderException {\n    if (outputBuffer == null) {\n      outputBuffer = decoder.dequeueOutputBuffer();\n      if (outputBuffer == null) {\n        return false;\n      }\n      decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;\n      buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;\n    }\n\n    if (outputBuffer.isEndOfStream()) {\n      if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {\n        // We're waiting to re-initialize the decoder, and have now processed all final buffers.\n        releaseDecoder();\n        maybeInitDecoder();\n      } else {\n        outputBuffer.release();\n        outputBuffer = null;\n        outputStreamEnded = true;\n      }\n      return false;\n    }\n\n    boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs);\n    if (processedOutputBuffer) {\n      onProcessedOutputBuffer(outputBuffer.timeUs);\n      outputBuffer = null;\n    }\n    return processedOutputBuffer;\n  }\n\n  /**\n   * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns\n   * whether it may be possible to process another output buffer.\n   *\n   * @param positionUs The player's current position.\n   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds,\n   *     measured at the start of the current iteration of the rendering loop.\n   * @return Whether it may be possible to drain another output buffer.\n   * @throws ExoPlaybackException If an error occurs processing the output buffer.\n   */\n  private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs)\n      throws ExoPlaybackException, VideoDecoderException {\n    if (initialPositionUs == C.TIME_UNSET) {\n      initialPositionUs = positionUs;\n    }\n\n    long earlyUs = outputBuffer.timeUs - positionUs;\n    if (!hasOutput()) {\n      // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.\n      if (isBufferLate(earlyUs)) {\n        skipOutputBuffer(outputBuffer);\n        return true;\n      }\n      return false;\n    }\n\n    long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs;\n    Format format = formatQueue.pollFloor(presentationTimeUs);\n    if (format != null) {\n      outputFormat = format;\n    }\n\n    long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;\n    boolean isStarted = getState() == STATE_STARTED;\n    if (!renderedFirstFrame\n        || (isStarted\n            && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {\n      renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat);\n      return true;\n    }\n\n    if (!isStarted || positionUs == initialPositionUs) {\n      return false;\n    }\n\n    if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)\n        && maybeDropBuffersToKeyframe(positionUs)) {\n      return false;\n    } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {\n      dropOutputBuffer(outputBuffer);\n      return true;\n    }\n\n    if (earlyUs < 30000) {\n      renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat);\n      return true;\n    }\n\n    return false;\n  }\n\n  private boolean hasOutput() {\n    return outputMode != C.VIDEO_OUTPUT_MODE_NONE;\n  }\n\n  private void onOutputChanged() {\n    // If we know the video size, report it again immediately.\n    maybeRenotifyVideoSizeChanged();\n    // We haven't rendered to the new output yet.\n    clearRenderedFirstFrame();\n    if (getState() == STATE_STARTED) {\n      setJoiningDeadlineMs();\n    }\n  }\n\n  private void onOutputRemoved() {\n    clearReportedVideoSize();\n    clearRenderedFirstFrame();\n  }\n\n  private void onOutputReset() {\n    // The output is unchanged and non-null. If we know the video size and/or have already\n    // rendered to the output, report these again immediately.\n    maybeRenotifyVideoSizeChanged();\n    maybeRenotifyRenderedFirstFrame();\n  }\n\n  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {\n    if (decoderDrmSession == null\n        || (!bufferEncrypted\n            && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = decoderDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw createRendererException(decoderDrmSession.getError(), inputFormat);\n    }\n    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;\n  }\n\n  private void setJoiningDeadlineMs() {\n    joiningDeadlineMs =\n        allowedJoiningTimeMs > 0\n            ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs)\n            : C.TIME_UNSET;\n  }\n\n  private void clearRenderedFirstFrame() {\n    renderedFirstFrame = false;\n  }\n\n  private void maybeNotifyRenderedFirstFrame() {\n    if (!renderedFirstFrame) {\n      renderedFirstFrame = true;\n      eventDispatcher.renderedFirstFrame(surface);\n    }\n  }\n\n  private void maybeRenotifyRenderedFirstFrame() {\n    if (renderedFirstFrame) {\n      eventDispatcher.renderedFirstFrame(surface);\n    }\n  }\n\n  private void clearReportedVideoSize() {\n    reportedWidth = Format.NO_VALUE;\n    reportedHeight = Format.NO_VALUE;\n  }\n\n  private void maybeNotifyVideoSizeChanged(int width, int height) {\n    if (reportedWidth != width || reportedHeight != height) {\n      reportedWidth = width;\n      reportedHeight = height;\n      eventDispatcher.videoSizeChanged(\n          width, height, /* unappliedRotationDegrees= */ 0, /* pixelWidthHeightRatio= */ 1);\n    }\n  }\n\n  private void maybeRenotifyVideoSizeChanged() {\n    if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {\n      eventDispatcher.videoSizeChanged(\n          reportedWidth,\n          reportedHeight,\n          /* unappliedRotationDegrees= */ 0,\n          /* pixelWidthHeightRatio= */ 1);\n    }\n  }\n\n  private void maybeNotifyDroppedFrames() {\n    if (droppedFrames > 0) {\n      long now = SystemClock.elapsedRealtime();\n      long elapsedMs = now - droppedFrameAccumulationStartTimeMs;\n      eventDispatcher.droppedFrames(droppedFrames, elapsedMs);\n      droppedFrames = 0;\n      droppedFrameAccumulationStartTimeMs = now;\n    }\n  }\n\n  private static boolean isBufferLate(long earlyUs) {\n    // Class a buffer as late if it should have been presented more than 30 ms ago.\n    return earlyUs < -30000;\n  }\n\n  private static boolean isBufferVeryLate(long earlyUs) {\n    // Class a buffer as very late if it should have been presented more than 500 ms ago.\n    return earlyUs < -500000;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\n/** Thrown when a video decoder error occurs. */\npublic class VideoDecoderException extends Exception {\n\n  /**\n   * Creates an instance with the given message.\n   *\n   * @param message The detail message for this exception.\n   */\n  public VideoDecoderException(String message) {\n    super(message);\n  }\n\n  /**\n   * Creates an instance with the given message and cause.\n   *\n   * @param message The detail message for this exception.\n   * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).\n   *     A <tt>null</tt> value is permitted, and indicates that the cause is nonexistent or unknown.\n   */\n  public VideoDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.util.AttributeSet;\nimport androidx.annotation.Nullable;\n\n/**\n * GLSurfaceView for rendering video output. To render video in this view, call {@link\n * #getVideoDecoderOutputBufferRenderer()} to get a {@link VideoDecoderOutputBufferRenderer} that\n * will render video decoder output buffers in this view.\n *\n * <p>This view is intended for use only with extension renderers. For other use cases a {@link\n * android.view.SurfaceView} or {@link android.view.TextureView} should be used instead.\n */\npublic class VideoDecoderGLSurfaceView extends GLSurfaceView {\n\n  private final VideoDecoderRenderer renderer;\n\n  /** @param context A {@link Context}. */\n  public VideoDecoderGLSurfaceView(Context context) {\n    this(context, /* attrs= */ null);\n  }\n\n  /**\n   * @param context A {@link Context}.\n   * @param attrs Custom attributes.\n   */\n  public VideoDecoderGLSurfaceView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n    renderer = new VideoDecoderRenderer(this);\n    setPreserveEGLContextOnPause(true);\n    setEGLContextClientVersion(2);\n    setRenderer(renderer);\n    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);\n  }\n\n  /** Returns the {@link VideoDecoderOutputBufferRenderer} that will render frames in this view. */\n  public VideoDecoderOutputBufferRenderer getVideoDecoderOutputBufferRenderer() {\n    return renderer;\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\n\n/** Input buffer to a video decoder. */\npublic class VideoDecoderInputBuffer extends DecoderInputBuffer {\n\n  @Nullable public ColorInfo colorInfo;\n\n  public VideoDecoderInputBuffer() {\n    super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.decoder.OutputBuffer;\nimport java.nio.ByteBuffer;\n\n/** Video decoder output buffer containing video frame data. */\npublic class VideoDecoderOutputBuffer extends OutputBuffer {\n\n  /** Buffer owner. */\n  public interface Owner {\n\n    /**\n     * Releases the buffer.\n     *\n     * @param outputBuffer Output buffer.\n     */\n    void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer);\n  }\n\n  // LINT.IfChange\n  public static final int COLORSPACE_UNKNOWN = 0;\n  public static final int COLORSPACE_BT601 = 1;\n  public static final int COLORSPACE_BT709 = 2;\n  public static final int COLORSPACE_BT2020 = 3;\n  // LINT.ThenChange(\n  //     ../../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc,\n  //     ../../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc\n  // )\n\n  /** Decoder private data. */\n  public int decoderPrivate;\n\n  /** Output mode. */\n  @C.VideoOutputMode public int mode;\n  /** RGB buffer for RGB mode. */\n  @Nullable public ByteBuffer data;\n\n  public int width;\n  public int height;\n  @Nullable public ColorInfo colorInfo;\n\n  /** YUV planes for YUV mode. */\n  @Nullable public ByteBuffer[] yuvPlanes;\n\n  @Nullable public int[] yuvStrides;\n  public int colorspace;\n\n  /**\n   * Supplemental data related to the output frame, if {@link #hasSupplementalData()} returns true.\n   * If present, the buffer is populated with supplemental data from position 0 to its limit.\n   */\n  @Nullable public ByteBuffer supplementalData;\n\n  private final Owner owner;\n\n  /**\n   * Creates VideoDecoderOutputBuffer.\n   *\n   * @param owner Buffer owner.\n   */\n  public VideoDecoderOutputBuffer(Owner owner) {\n    this.owner = owner;\n  }\n\n  @Override\n  public void release() {\n    owner.releaseOutputBuffer(this);\n  }\n\n  /**\n   * Initializes the buffer.\n   *\n   * @param timeUs The presentation timestamp for the buffer, in microseconds.\n   * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link\n   *     C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}.\n   * @param supplementalData Supplemental data associated with the frame, or {@code null} if not\n   *     present. It is safe to reuse the provided buffer after this method returns.\n   */\n  public void init(\n      long timeUs, @C.VideoOutputMode int mode, @Nullable ByteBuffer supplementalData) {\n    this.timeUs = timeUs;\n    this.mode = mode;\n    if (supplementalData != null && supplementalData.hasRemaining()) {\n      addFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);\n      int size = supplementalData.limit();\n      if (this.supplementalData == null || this.supplementalData.capacity() < size) {\n        this.supplementalData = ByteBuffer.allocate(size);\n      } else {\n        this.supplementalData.clear();\n      }\n      this.supplementalData.put(supplementalData);\n      this.supplementalData.flip();\n      supplementalData.position(0);\n    } else {\n      this.supplementalData = null;\n    }\n  }\n\n  /**\n   * Resizes the buffer based on the given stride. Called via JNI after decoding completes.\n   *\n   * @return Whether the buffer was resized successfully.\n   */\n  public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {\n    this.width = width;\n    this.height = height;\n    this.colorspace = colorspace;\n    int uvHeight = (int) (((long) height + 1) / 2);\n    if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {\n      return false;\n    }\n    int yLength = yStride * height;\n    int uvLength = uvStride * uvHeight;\n    int minimumYuvSize = yLength + (uvLength * 2);\n    if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {\n      return false;\n    }\n\n    // Initialize data.\n    if (data == null || data.capacity() < minimumYuvSize) {\n      data = ByteBuffer.allocateDirect(minimumYuvSize);\n    } else {\n      data.position(0);\n      data.limit(minimumYuvSize);\n    }\n\n    if (yuvPlanes == null) {\n      yuvPlanes = new ByteBuffer[3];\n    }\n\n    ByteBuffer data = this.data;\n    ByteBuffer[] yuvPlanes = this.yuvPlanes;\n\n    // Rewrapping has to be done on every frame since the stride might have changed.\n    yuvPlanes[0] = data.slice();\n    yuvPlanes[0].limit(yLength);\n    data.position(yLength);\n    yuvPlanes[1] = data.slice();\n    yuvPlanes[1].limit(uvLength);\n    data.position(yLength + uvLength);\n    yuvPlanes[2] = data.slice();\n    yuvPlanes[2].limit(uvLength);\n    if (yuvStrides == null) {\n      yuvStrides = new int[3];\n    }\n    yuvStrides[0] = yStride;\n    yuvStrides[1] = uvStride;\n    yuvStrides[2] = uvStride;\n    return true;\n  }\n\n  /**\n   * Configures the buffer for the given frame dimensions when passing actual frame data via {@link\n   * #decoderPrivate}. Called via JNI after decoding completes.\n   */\n  public void initForPrivateFrame(int width, int height) {\n    this.width = width;\n    this.height = height;\n  }\n\n  /**\n   * Ensures that the result of multiplying individual numbers can fit into the size limit of an\n   * integer.\n   */\n  private static boolean isSafeToMultiply(int a, int b) {\n    return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\n/** Renders the {@link VideoDecoderOutputBuffer}. */\npublic interface VideoDecoderOutputBufferRenderer {\n\n  /**\n   * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer.\n   *\n   * @param outputBuffer The output buffer to be rendered.\n   */\n  void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.opengl.GLES20;\nimport android.opengl.GLSurfaceView;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.GlUtil;\nimport java.nio.FloatBuffer;\nimport java.util.concurrent.atomic.AtomicReference;\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\n/**\n * GLSurfaceView.Renderer implementation that can render YUV Frames returned by a video decoder\n * after decoding. It does the YUV to RGB color conversion in the Fragment Shader.\n */\n/* package */ class VideoDecoderRenderer\n    implements GLSurfaceView.Renderer, VideoDecoderOutputBufferRenderer {\n\n  private static final float[] kColorConversion601 = {\n    1.164f, 1.164f, 1.164f,\n    0.0f, -0.392f, 2.017f,\n    1.596f, -0.813f, 0.0f,\n  };\n\n  private static final float[] kColorConversion709 = {\n    1.164f, 1.164f, 1.164f,\n    0.0f, -0.213f, 2.112f,\n    1.793f, -0.533f, 0.0f,\n  };\n\n  private static final float[] kColorConversion2020 = {\n    1.168f, 1.168f, 1.168f,\n    0.0f, -0.188f, 2.148f,\n    1.683f, -0.652f, 0.0f,\n  };\n\n  private static final String VERTEX_SHADER =\n      \"varying vec2 interp_tc_y;\\n\"\n          + \"varying vec2 interp_tc_u;\\n\"\n          + \"varying vec2 interp_tc_v;\\n\"\n          + \"attribute vec4 in_pos;\\n\"\n          + \"attribute vec2 in_tc_y;\\n\"\n          + \"attribute vec2 in_tc_u;\\n\"\n          + \"attribute vec2 in_tc_v;\\n\"\n          + \"void main() {\\n\"\n          + \"  gl_Position = in_pos;\\n\"\n          + \"  interp_tc_y = in_tc_y;\\n\"\n          + \"  interp_tc_u = in_tc_u;\\n\"\n          + \"  interp_tc_v = in_tc_v;\\n\"\n          + \"}\\n\";\n  private static final String[] TEXTURE_UNIFORMS = {\"y_tex\", \"u_tex\", \"v_tex\"};\n  private static final String FRAGMENT_SHADER =\n      \"precision mediump float;\\n\"\n          + \"varying vec2 interp_tc_y;\\n\"\n          + \"varying vec2 interp_tc_u;\\n\"\n          + \"varying vec2 interp_tc_v;\\n\"\n          + \"uniform sampler2D y_tex;\\n\"\n          + \"uniform sampler2D u_tex;\\n\"\n          + \"uniform sampler2D v_tex;\\n\"\n          + \"uniform mat3 mColorConversion;\\n\"\n          + \"void main() {\\n\"\n          + \"  vec3 yuv;\\n\"\n          + \"  yuv.x = texture2D(y_tex, interp_tc_y).r - 0.0625;\\n\"\n          + \"  yuv.y = texture2D(u_tex, interp_tc_u).r - 0.5;\\n\"\n          + \"  yuv.z = texture2D(v_tex, interp_tc_v).r - 0.5;\\n\"\n          + \"  gl_FragColor = vec4(mColorConversion * yuv, 1.0);\\n\"\n          + \"}\\n\";\n\n  private static final FloatBuffer TEXTURE_VERTICES =\n      GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f});\n  private final GLSurfaceView surfaceView;\n  private final int[] yuvTextures = new int[3];\n  private final AtomicReference<VideoDecoderOutputBuffer> pendingOutputBufferReference;\n\n  // Kept in field rather than a local variable in order not to get garbage collected before\n  // glDrawArrays uses it.\n  private FloatBuffer[] textureCoords;\n\n  private int program;\n  private int[] texLocations;\n  private int colorMatrixLocation;\n  private int[] previousWidths;\n  private int[] previousStrides;\n\n  @Nullable\n  private VideoDecoderOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread.\n\n  public VideoDecoderRenderer(GLSurfaceView surfaceView) {\n    this.surfaceView = surfaceView;\n    pendingOutputBufferReference = new AtomicReference<>();\n    textureCoords = new FloatBuffer[3];\n    texLocations = new int[3];\n    previousWidths = new int[3];\n    previousStrides = new int[3];\n    for (int i = 0; i < 3; i++) {\n      previousWidths[i] = previousStrides[i] = -1;\n    }\n  }\n\n  @Override\n  public void onSurfaceCreated(GL10 unused, EGLConfig config) {\n    program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER);\n    GLES20.glUseProgram(program);\n    int posLocation = GLES20.glGetAttribLocation(program, \"in_pos\");\n    GLES20.glEnableVertexAttribArray(posLocation);\n    GLES20.glVertexAttribPointer(posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES);\n    texLocations[0] = GLES20.glGetAttribLocation(program, \"in_tc_y\");\n    GLES20.glEnableVertexAttribArray(texLocations[0]);\n    texLocations[1] = GLES20.glGetAttribLocation(program, \"in_tc_u\");\n    GLES20.glEnableVertexAttribArray(texLocations[1]);\n    texLocations[2] = GLES20.glGetAttribLocation(program, \"in_tc_v\");\n    GLES20.glEnableVertexAttribArray(texLocations[2]);\n    GlUtil.checkGlError();\n    colorMatrixLocation = GLES20.glGetUniformLocation(program, \"mColorConversion\");\n    GlUtil.checkGlError();\n    setupTextures();\n    GlUtil.checkGlError();\n  }\n\n  @Override\n  public void onSurfaceChanged(GL10 unused, int width, int height) {\n    GLES20.glViewport(0, 0, width, height);\n  }\n\n  @Override\n  public void onDrawFrame(GL10 unused) {\n    VideoDecoderOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null);\n    if (pendingOutputBuffer == null && renderedOutputBuffer == null) {\n      // There is no output buffer to render at the moment.\n      return;\n    }\n    if (pendingOutputBuffer != null) {\n      if (renderedOutputBuffer != null) {\n        renderedOutputBuffer.release();\n      }\n      renderedOutputBuffer = pendingOutputBuffer;\n    }\n    VideoDecoderOutputBuffer outputBuffer = renderedOutputBuffer;\n    // Set color matrix. Assume BT709 if the color space is unknown.\n    float[] colorConversion = kColorConversion709;\n    switch (outputBuffer.colorspace) {\n      case VideoDecoderOutputBuffer.COLORSPACE_BT601:\n        colorConversion = kColorConversion601;\n        break;\n      case VideoDecoderOutputBuffer.COLORSPACE_BT2020:\n        colorConversion = kColorConversion2020;\n        break;\n      case VideoDecoderOutputBuffer.COLORSPACE_BT709:\n      default:\n        break; // Do nothing\n    }\n    GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0);\n\n    for (int i = 0; i < 3; i++) {\n      int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2;\n      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);\n      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);\n      GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);\n      GLES20.glTexImage2D(\n          GLES20.GL_TEXTURE_2D,\n          0,\n          GLES20.GL_LUMINANCE,\n          outputBuffer.yuvStrides[i],\n          h,\n          0,\n          GLES20.GL_LUMINANCE,\n          GLES20.GL_UNSIGNED_BYTE,\n          outputBuffer.yuvPlanes[i]);\n    }\n\n    int[] widths = new int[3];\n    widths[0] = outputBuffer.width;\n    // TODO: Handle streams where chroma channels are not stored at half width and height\n    // compared to luma channel. See [Internal: b/142097774].\n    // U and V planes are being stored at half width compared to Y.\n    widths[1] = widths[2] = (widths[0] + 1) / 2;\n    for (int i = 0; i < 3; i++) {\n      // Set cropping of stride if either width or stride has changed.\n      if (previousWidths[i] != widths[i] || previousStrides[i] != outputBuffer.yuvStrides[i]) {\n        Assertions.checkState(outputBuffer.yuvStrides[i] != 0);\n        float widthRatio = (float) widths[i] / outputBuffer.yuvStrides[i];\n        // These buffers are consumed during each call to glDrawArrays. They need to be member\n        // variables rather than local variables in order not to get garbage collected.\n        textureCoords[i] =\n            GlUtil.createBuffer(\n                new float[] {0.0f, 0.0f, 0.0f, 1.0f, widthRatio, 0.0f, widthRatio, 1.0f});\n        GLES20.glVertexAttribPointer(\n            texLocations[i], 2, GLES20.GL_FLOAT, false, 0, textureCoords[i]);\n        previousWidths[i] = widths[i];\n        previousStrides[i] = outputBuffer.yuvStrides[i];\n      }\n    }\n\n    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n    GlUtil.checkGlError();\n  }\n\n  @Override\n  public void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {\n    VideoDecoderOutputBuffer oldPendingOutputBuffer =\n        pendingOutputBufferReference.getAndSet(outputBuffer);\n    if (oldPendingOutputBuffer != null) {\n      // The old pending output buffer will never be used for rendering, so release it now.\n      oldPendingOutputBuffer.release();\n    }\n    surfaceView.requestRender();\n  }\n\n  private void setupTextures() {\n    GLES20.glGenTextures(3, yuvTextures, 0);\n    for (int i = 0; i < 3; i++) {\n      GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i);\n      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);\n      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);\n      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n      GLES20.glTexParameterf(\n          GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);\n      GLES20.glTexParameterf(\n          GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);\n    }\n    GlUtil.checkGlError();\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.media.MediaFormat;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\n\n/** A listener for metadata corresponding to video frame being rendered. */\npublic interface VideoFrameMetadataListener {\n  /**\n   * Called when the video frame about to be rendered. This method is called on the playback thread.\n   *\n   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.\n   * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.\n   *     If the platform API version of the device is less than 21, then this is the best effort.\n   * @param format The format associated with the frame.\n   * @param mediaFormat The framework media format associated with the frame, or {@code null} if not\n   *     known or not applicable (e.g., because the frame was not output by a {@link\n   *     android.media.MediaCodec MediaCodec}).\n   */\n  void onVideoFrameAboutToBeRendered(\n          long presentationTimeUs,\n          long releaseTimeNs,\n          Format format,\n          @Nullable MediaFormat mediaFormat);\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.hardware.display.DisplayManager;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport android.view.Choreographer;\nimport android.view.Choreographer.FrameCallback;\nimport android.view.Display;\nimport android.view.WindowManager;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Makes a best effort to adjust frame release timestamps for a smoother visual result.\n */\npublic final class VideoFrameReleaseTimeHelper {\n\n  private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;\n  private static final long MAX_ALLOWED_DRIFT_NS = 20000000;\n\n  private static final long VSYNC_OFFSET_PERCENTAGE = 80;\n  private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;\n\n  private final WindowManager windowManager;\n  private final VSyncSampler vsyncSampler;\n  private final DefaultDisplayListener displayListener;\n\n  private long vsyncDurationNs;\n  private long vsyncOffsetNs;\n\n  private long lastFramePresentationTimeUs;\n  private long adjustedLastFrameTimeNs;\n  private long pendingAdjustedFrameTimeNs;\n\n  private boolean haveSync;\n  private long syncUnadjustedReleaseTimeNs;\n  private long syncFramePresentationTimeNs;\n  private long frameCount;\n\n  /**\n   * Constructs an instance that smooths frame release timestamps but does not align them with\n   * the default display's vsync signal.\n   */\n  public VideoFrameReleaseTimeHelper() {\n    this(null);\n  }\n\n  /**\n   * Constructs an instance that smooths frame release timestamps and aligns them with the default\n   * display's vsync signal.\n   *\n   * @param context A context from which information about the default display can be retrieved.\n   */\n  public VideoFrameReleaseTimeHelper(@Nullable Context context) {\n    if (context != null) {\n      context = context.getApplicationContext();\n      windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n    } else {\n      windowManager = null;\n    }\n    if (windowManager != null) {\n      displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null;\n      vsyncSampler = VSyncSampler.getInstance();\n    } else {\n      displayListener = null;\n      vsyncSampler = null;\n    }\n    vsyncDurationNs = C.TIME_UNSET;\n    vsyncOffsetNs = C.TIME_UNSET;\n  }\n\n  /**\n   * Enables the helper. Must be called from the playback thread.\n   */\n  public void enable() {\n    haveSync = false;\n    if (windowManager != null) {\n      vsyncSampler.addObserver();\n      if (displayListener != null) {\n        displayListener.register();\n      }\n      updateDefaultDisplayRefreshRateParams();\n    }\n  }\n\n  /**\n   * Disables the helper. Must be called from the playback thread.\n   */\n  public void disable() {\n    if (windowManager != null) {\n      if (displayListener != null) {\n        displayListener.unregister();\n      }\n      vsyncSampler.removeObserver();\n    }\n  }\n\n  /**\n   * Adjusts a frame release timestamp. Must be called from the playback thread.\n   *\n   * @param framePresentationTimeUs The frame's presentation time, in microseconds.\n   * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in\n   *     the same time base as {@link System#nanoTime()}.\n   * @return The adjusted frame release timestamp, in nanoseconds and in the same time base as\n   *     {@link System#nanoTime()}.\n   */\n  public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {\n    long framePresentationTimeNs = framePresentationTimeUs * 1000;\n\n    // Until we know better, the adjustment will be a no-op.\n    long adjustedFrameTimeNs = framePresentationTimeNs;\n    long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;\n\n    if (haveSync) {\n      // See if we've advanced to the next frame.\n      if (framePresentationTimeUs != lastFramePresentationTimeUs) {\n        frameCount++;\n        adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;\n      }\n      if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {\n        // We're synced and have waited the required number of frames to apply an adjustment.\n        // Calculate the average frame time across all the frames we've seen since the last sync.\n        // This will typically give us a frame rate at a finer granularity than the frame times\n        // themselves (which often only have millisecond granularity).\n        long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs)\n            / frameCount;\n        // Project the adjusted frame time forward using the average.\n        long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;\n\n        if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {\n          haveSync = false;\n        } else {\n          adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;\n          adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs\n              - syncFramePresentationTimeNs;\n        }\n      } else {\n        // We're synced but haven't waited the required number of frames to apply an adjustment.\n        // Check drift anyway.\n        if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {\n          haveSync = false;\n        }\n      }\n    }\n\n    // If we need to sync, do so now.\n    if (!haveSync) {\n      syncFramePresentationTimeNs = framePresentationTimeNs;\n      syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;\n      frameCount = 0;\n      haveSync = true;\n    }\n\n    lastFramePresentationTimeUs = framePresentationTimeUs;\n    pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;\n\n    if (vsyncSampler == null || vsyncDurationNs == C.TIME_UNSET) {\n      return adjustedReleaseTimeNs;\n    }\n    long sampledVsyncTimeNs = vsyncSampler.sampledVsyncTimeNs;\n    if (sampledVsyncTimeNs == C.TIME_UNSET) {\n      return adjustedReleaseTimeNs;\n    }\n\n    // Find the timestamp of the closest vsync. This is the vsync that we're targeting.\n    long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs);\n    // Apply an offset so that we release before the target vsync, but after the previous one.\n    return snappedTimeNs - vsyncOffsetNs;\n  }\n\n  @TargetApi(17)\n  private DefaultDisplayListener maybeBuildDefaultDisplayListenerV17(Context context) {\n    DisplayManager manager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);\n    return manager == null ? null : new DefaultDisplayListener(manager);\n  }\n\n  private void updateDefaultDisplayRefreshRateParams() {\n    // Note: If we fail to update the parameters, we leave them set to their previous values.\n    Display defaultDisplay = windowManager.getDefaultDisplay();\n    if (defaultDisplay != null) {\n      double defaultDisplayRefreshRate = defaultDisplay.getRefreshRate();\n      vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate);\n      vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;\n    }\n  }\n\n  private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {\n    long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;\n    long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;\n    return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;\n  }\n\n  private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {\n    long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;\n    long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);\n    long snappedBeforeNs;\n    long snappedAfterNs;\n    if (releaseTime <= snappedTimeNs) {\n      snappedBeforeNs = snappedTimeNs - vsyncDuration;\n      snappedAfterNs = snappedTimeNs;\n    } else {\n      snappedBeforeNs = snappedTimeNs;\n      snappedAfterNs = snappedTimeNs + vsyncDuration;\n    }\n    long snappedAfterDiff = snappedAfterNs - releaseTime;\n    long snappedBeforeDiff = releaseTime - snappedBeforeNs;\n    return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;\n  }\n\n  @TargetApi(17)\n  private final class DefaultDisplayListener implements DisplayManager.DisplayListener {\n\n    private final DisplayManager displayManager;\n\n    public DefaultDisplayListener(DisplayManager displayManager) {\n      this.displayManager = displayManager;\n    }\n\n    public void register() {\n      displayManager.registerDisplayListener(this, null);\n    }\n\n    public void unregister() {\n      displayManager.unregisterDisplayListener(this);\n    }\n\n    @Override\n    public void onDisplayAdded(int displayId) {\n      // Do nothing.\n    }\n\n    @Override\n    public void onDisplayRemoved(int displayId) {\n      // Do nothing.\n    }\n\n    @Override\n    public void onDisplayChanged(int displayId) {\n      if (displayId == Display.DEFAULT_DISPLAY) {\n        updateDefaultDisplayRefreshRateParams();\n      }\n    }\n\n  }\n\n  /**\n   * Samples display vsync timestamps. A single instance using a single {@link Choreographer} is\n   * shared by all {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a resource\n   * leak in the platform on API levels prior to 23. See [Internal: b/12455729].\n   */\n  private static final class VSyncSampler implements FrameCallback, Handler.Callback {\n\n    public volatile long sampledVsyncTimeNs;\n\n    private static final int CREATE_CHOREOGRAPHER = 0;\n    private static final int MSG_ADD_OBSERVER = 1;\n    private static final int MSG_REMOVE_OBSERVER = 2;\n\n    private static final VSyncSampler INSTANCE = new VSyncSampler();\n\n    private final Handler handler;\n    private final HandlerThread choreographerOwnerThread;\n    private Choreographer choreographer;\n    private int observerCount;\n\n    public static VSyncSampler getInstance() {\n      return INSTANCE;\n    }\n\n    private VSyncSampler() {\n      sampledVsyncTimeNs = C.TIME_UNSET;\n      choreographerOwnerThread = new HandlerThread(\"ChoreographerOwner:Handler\");\n      choreographerOwnerThread.start();\n      handler = Util.createHandler(choreographerOwnerThread.getLooper(), /* callback= */ this);\n      handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);\n    }\n\n    /**\n     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is observing\n     * {@link #sampledVsyncTimeNs}, and hence that the value should be periodically updated.\n     */\n    public void addObserver() {\n      handler.sendEmptyMessage(MSG_ADD_OBSERVER);\n    }\n\n    /**\n     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no longer observing\n     * {@link #sampledVsyncTimeNs}.\n     */\n    public void removeObserver() {\n      handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);\n    }\n\n    @Override\n    public void doFrame(long vsyncTimeNs) {\n      sampledVsyncTimeNs = vsyncTimeNs;\n      choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);\n    }\n\n    @Override\n    public boolean handleMessage(Message message) {\n      switch (message.what) {\n        case CREATE_CHOREOGRAPHER: {\n          createChoreographerInstanceInternal();\n          return true;\n        }\n        case MSG_ADD_OBSERVER: {\n          addObserverInternal();\n          return true;\n        }\n        case MSG_REMOVE_OBSERVER: {\n          removeObserverInternal();\n          return true;\n        }\n        default: {\n          return false;\n        }\n      }\n    }\n\n    private void createChoreographerInstanceInternal() {\n      choreographer = Choreographer.getInstance();\n    }\n\n    private void addObserverInternal() {\n      observerCount++;\n      if (observerCount == 1) {\n        choreographer.postFrameCallback(this);\n      }\n    }\n\n    private void removeObserverInternal() {\n      observerCount--;\n      if (observerCount == 0) {\n        choreographer.removeFrameCallback(this);\n        sampledVsyncTimeNs = C.TIME_UNSET;\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\n/** A listener for metadata corresponding to video being rendered. */\npublic interface VideoListener {\n\n  /**\n   * Called each time there's a change in the size of the video being rendered.\n   *\n   * @param width The video width in pixels.\n   * @param height The video height in pixels.\n   * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise\n   *     rotation in degrees that the application should apply for the video for it to be rendered\n   *     in the correct orientation. This value will always be zero on API levels 21 and above,\n   *     since the renderer will apply all necessary rotations internally. On earlier API levels\n   *     this is not possible. Applications that use {@link android.view.TextureView} can apply the\n   *     rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not\n   *     expect to encounter rotated videos can safely ignore this parameter.\n   * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of\n   *     square pixels this will be equal to 1.0. Different values are indicative of anamorphic\n   *     content.\n   */\n  default void onVideoSizeChanged(\n          int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {}\n\n  /**\n   * Called each time there's a change in the size of the surface onto which the video is being\n   * rendered.\n   *\n   * @param width The surface width in pixels. May be {@link\n   *     com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered\n   *     onto a surface.\n   * @param height The surface height in pixels. May be {@link\n   *     com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered\n   *     onto a surface.\n   */\n  default void onSurfaceSizeChanged(int width, int height) {}\n\n  /**\n   * Called when a frame is rendered for the first time since setting the surface, and when a frame\n   * is rendered for the first time since a video track was selected.\n   */\n  default void onRenderedFirstFrame() {}\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video;\n\nimport static com.google.android.exoplayer2.util.Util.castNonNull;\n\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.view.Surface;\nimport android.view.TextureView;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/**\n * Listener of video {@link Renderer} events. All methods have no-op default implementations to\n * allow selective overrides.\n */\npublic interface VideoRendererEventListener {\n\n  /**\n   * Called when the renderer is enabled.\n   *\n   * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it\n   *     remains enabled.\n   */\n  default void onVideoEnabled(DecoderCounters counters) {}\n\n  /**\n   * Called when a decoder is created.\n   *\n   * @param decoderName The decoder that was created.\n   * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization\n   *     finished.\n   * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.\n   */\n  default void onVideoDecoderInitialized(\n          String decoderName, long initializedTimestampMs, long initializationDurationMs) {}\n\n  /**\n   * Called when the format of the media being consumed by the renderer changes.\n   *\n   * @param format The new format.\n   */\n  default void onVideoInputFormatChanged(Format format) {}\n\n  /**\n   * Called to report the number of frames dropped by the renderer. Dropped frames are reported\n   * whenever the renderer is stopped having dropped frames, and optionally, whenever the count\n   * reaches a specified threshold whilst the renderer is started.\n   *\n   * @param count The number of dropped frames.\n   * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration\n   *     is timed from when the renderer was started or from when dropped frames were last reported\n   *     (whichever was more recent), and not from when the first of the reported drops occurred.\n   */\n  default void onDroppedFrames(int count, long elapsedMs) {}\n\n  /**\n   * Called before a frame is rendered for the first time since setting the surface, and each time\n   * there's a change in the size, rotation or pixel aspect ratio of the video being rendered.\n   *\n   * @param width The video width in pixels.\n   * @param height The video height in pixels.\n   * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise\n   *     rotation in degrees that the application should apply for the video for it to be rendered\n   *     in the correct orientation. This value will always be zero on API levels 21 and above,\n   *     since the renderer will apply all necessary rotations internally. On earlier API levels\n   *     this is not possible. Applications that use {@link TextureView} can apply the rotation by\n   *     calling {@link TextureView#setTransform}. Applications that do not expect to encounter\n   *     rotated videos can safely ignore this parameter.\n   * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of\n   *     square pixels this will be equal to 1.0. Different values are indicative of anamorphic\n   *     content.\n   */\n  default void onVideoSizeChanged(\n          int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {}\n\n  /**\n   * Called when a frame is rendered for the first time since setting the surface, and when a frame\n   * is rendered for the first time since the renderer was reset.\n   *\n   * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if\n   *     the renderer renders to something that isn't a {@link Surface}.\n   */\n  default void onRenderedFirstFrame(@Nullable Surface surface) {}\n\n  /**\n   * Called when the renderer is disabled.\n   *\n   * @param counters {@link DecoderCounters} that were updated by the renderer.\n   */\n  default void onVideoDisabled(DecoderCounters counters) {}\n\n  /**\n   * Dispatches events to a {@link VideoRendererEventListener}.\n   */\n  final class EventDispatcher {\n\n    @Nullable private final Handler handler;\n    @Nullable private final VideoRendererEventListener listener;\n\n    /**\n     * @param handler A handler for dispatching events, or null if creating a dummy instance.\n     * @param listener The listener to which events should be dispatched, or null if creating a\n     *     dummy instance.\n     */\n    public EventDispatcher(@Nullable Handler handler,\n        @Nullable VideoRendererEventListener listener) {\n      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;\n      this.listener = listener;\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. */\n    public void enabled(DecoderCounters decoderCounters) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. */\n    public void decoderInitialized(\n        String decoderName, long initializedTimestampMs, long initializationDurationMs) {\n      if (handler != null) {\n        handler.post(\n            () ->\n                castNonNull(listener)\n                    .onVideoDecoderInitialized(\n                        decoderName, initializedTimestampMs, initializationDurationMs));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */\n    public void inputFormatChanged(Format format) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */\n    public void droppedFrames(int droppedFrameCount, long elapsedMs) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onDroppedFrames(droppedFrameCount, elapsedMs));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}. */\n    public void videoSizeChanged(\n        int width,\n        int height,\n        final int unappliedRotationDegrees,\n        final float pixelWidthHeightRatio) {\n      if (handler != null) {\n        handler.post(\n            () ->\n                castNonNull(listener)\n                    .onVideoSizeChanged(\n                        width, height, unappliedRotationDegrees, pixelWidthHeightRatio));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */\n    public void renderedFirstFrame(@Nullable Surface surface) {\n      if (handler != null) {\n        handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */\n    public void disabled(DecoderCounters counters) {\n      counters.ensureUpdated();\n      if (handler != null) {\n        handler.post(\n            () -> {\n              counters.ensureUpdated();\n              castNonNull(listener).onVideoDisabled(counters);\n            });\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.video;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video.spherical;\n\n/** Listens camera motion. */\npublic interface CameraMotionListener {\n\n  /**\n   * Called when a new camera motion is read. This method is called on the playback thread.\n   *\n   * @param timeUs The presentation time of the data.\n   * @param rotation Angle axis orientation in radians representing the rotation from camera\n   *     coordinate system to world coordinate system.\n   */\n  void onCameraMotion(long timeUs, float[] rotation);\n\n  /** Called when the camera motion track position is reset or the track is disabled. */\n  void onCameraMotionReset();\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video.spherical;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BaseRenderer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.nio.ByteBuffer;\n\n/** A {@link Renderer} that parses the camera motion track. */\npublic class CameraMotionRenderer extends BaseRenderer {\n\n  // The amount of time to read samples ahead of the current time.\n  private static final int SAMPLE_WINDOW_DURATION_US = 100000;\n\n  private final DecoderInputBuffer buffer;\n  private final ParsableByteArray scratch;\n\n  private long offsetUs;\n  @Nullable private CameraMotionListener listener;\n  private long lastTimestampUs;\n\n  public CameraMotionRenderer() {\n    super(C.TRACK_TYPE_CAMERA_MOTION);\n    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n    scratch = new ParsableByteArray();\n  }\n\n  @Override\n  @Capabilities\n  public int supportsFormat(Format format) {\n    return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType)\n        ? RendererCapabilities.create(FORMAT_HANDLED)\n        : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);\n  }\n\n  @Override\n  public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {\n    if (messageType == C.MSG_SET_CAMERA_MOTION_LISTENER) {\n      listener = (CameraMotionListener) message;\n    } else {\n      super.handleMessage(messageType, message);\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\n    this.offsetUs = offsetUs;\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    resetListener();\n  }\n\n  @Override\n  protected void onDisabled() {\n    resetListener();\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    // Keep reading available samples as long as the sample time is not too far into the future.\n    while (!hasReadStreamToEnd() && lastTimestampUs < positionUs + SAMPLE_WINDOW_DURATION_US) {\n      buffer.clear();\n      FormatHolder formatHolder = getFormatHolder();\n      int result = readSource(formatHolder, buffer, /* formatRequired= */ false);\n      if (result != C.RESULT_BUFFER_READ || buffer.isEndOfStream()) {\n        return;\n      }\n\n      buffer.flip();\n      lastTimestampUs = buffer.timeUs;\n      if (listener != null) {\n        float[] rotation = parseMetadata(Util.castNonNull(buffer.data));\n        if (rotation != null) {\n          Util.castNonNull(listener).onCameraMotion(lastTimestampUs - offsetUs, rotation);\n        }\n      }\n    }\n  }\n\n  @Override\n  public boolean isEnded() {\n    return hasReadStreamToEnd();\n  }\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  private @Nullable float[] parseMetadata(ByteBuffer data) {\n    if (data.remaining() != 16) {\n      return null;\n    }\n    scratch.reset(data.array(), data.limit());\n    scratch.setPosition(data.arrayOffset() + 4); // skip reserved bytes too.\n    float[] result = new float[3];\n    for (int i = 0; i < 3; i++) {\n      result[i] = Float.intBitsToFloat(scratch.readLittleEndianInt());\n    }\n    return result;\n  }\n\n  private void resetListener() {\n    lastTimestampUs = 0;\n    if (listener != null) {\n      listener.onCameraMotionReset();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video.spherical;\n\nimport android.opengl.Matrix;\nimport com.google.android.exoplayer2.util.TimedValueQueue;\n\n/**\n * This class serves multiple purposes:\n *\n * <ul>\n *   <li>Queues the rotation metadata extracted from camera motion track.\n *   <li>Converts the metadata to rotation matrices in OpenGl coordinate system.\n *   <li>Recenters the rotations to componsate the yaw of the initial rotation.\n * </ul>\n */\npublic final class FrameRotationQueue {\n  private final float[] recenterMatrix;\n  private final float[] rotationMatrix;\n  private final TimedValueQueue<float[]> rotations;\n  private boolean recenterMatrixComputed;\n\n  public FrameRotationQueue() {\n    recenterMatrix = new float[16];\n    rotationMatrix = new float[16];\n    rotations = new TimedValueQueue<>();\n  }\n\n  /**\n   * Sets a rotation for a given timestamp.\n   *\n   * @param timestampUs Timestamp of the rotation.\n   * @param angleAxis Angle axis orientation in radians representing the rotation from camera\n   *     coordinate system to world coordinate system.\n   */\n  public void setRotation(long timestampUs, float[] angleAxis) {\n    rotations.add(timestampUs, angleAxis);\n  }\n\n  /** Removes all of the rotations and forces rotations to be recentered. */\n  public void reset() {\n    rotations.clear();\n    recenterMatrixComputed = false;\n  }\n\n  /**\n   * Copies the rotation matrix with the greatest timestamp which is less than or equal to the given\n   * timestamp to {@code matrix}. Removes all older rotations and the returned one from the queue.\n   * Does nothing if there is no such rotation.\n   *\n   * @param matrix The rotation matrix.\n   * @param timestampUs The time in microseconds to query the rotation.\n   * @return Whether a rotation matrix is copied to {@code matrix}.\n   */\n  public boolean pollRotationMatrix(float[] matrix, long timestampUs) {\n    float[] rotation = rotations.pollFloor(timestampUs);\n    if (rotation == null) {\n      return false;\n    }\n    // TODO [Internal: b/113315546]: Slerp between the floor and ceil rotation.\n    getRotationMatrixFromAngleAxis(rotationMatrix, rotation);\n    if (!recenterMatrixComputed) {\n      computeRecenterMatrix(recenterMatrix, rotationMatrix);\n      recenterMatrixComputed = true;\n    }\n    Matrix.multiplyMM(matrix, 0, recenterMatrix, 0, rotationMatrix, 0);\n    return true;\n  }\n\n  /**\n   * Computes a recentering matrix from the given angle-axis rotation only accounting for yaw. Roll\n   * and tilt will not be compensated.\n   *\n   * @param recenterMatrix The recenter matrix.\n   * @param rotationMatrix The rotation matrix.\n   */\n  public static void computeRecenterMatrix(float[] recenterMatrix, float[] rotationMatrix) {\n    // The re-centering matrix is computed as follows:\n    // recenter.row(2) = temp.col(2).transpose();\n    // recenter.row(0) = recenter.row(1).cross(recenter.row(2)).normalized();\n    // recenter.row(2) = recenter.row(0).cross(recenter.row(1)).normalized();\n    //             | temp[10]  0   -temp[8]    0|\n    //             | 0         1    0          0|\n    // recenter =  | temp[8]   0    temp[10]   0|\n    //             | 0         0    0          1|\n    Matrix.setIdentityM(recenterMatrix, 0);\n    float normRowSqr =\n        rotationMatrix[10] * rotationMatrix[10] + rotationMatrix[8] * rotationMatrix[8];\n    float normRow = (float) Math.sqrt(normRowSqr);\n    recenterMatrix[0] = rotationMatrix[10] / normRow;\n    recenterMatrix[2] = rotationMatrix[8] / normRow;\n    recenterMatrix[8] = -rotationMatrix[8] / normRow;\n    recenterMatrix[10] = rotationMatrix[10] / normRow;\n  }\n\n  private static void getRotationMatrixFromAngleAxis(float[] matrix, float[] angleAxis) {\n    // Convert coordinates to OpenGL coordinates.\n    // CAMM motion metadata: +x right, +y down, and +z forward.\n    // OpenGL: +x right, +y up, -z forwards\n    float x = angleAxis[0];\n    float y = -angleAxis[1];\n    float z = -angleAxis[2];\n    float angleRad = Matrix.length(x, y, z);\n    if (angleRad != 0) {\n      float angleDeg = (float) Math.toDegrees(angleRad);\n      Matrix.setRotateM(matrix, 0, angleDeg, x / angleRad, y / angleRad, z / angleRad);\n    } else {\n      Matrix.setIdentityM(matrix, 0);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/Projection.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video.spherical;\n\nimport androidx.annotation.IntDef;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.C.StereoMode;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/** The projection mesh used with 360/VR videos. */\npublic final class Projection {\n\n  /** Enforces allowed (sub) mesh draw modes. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({DRAW_MODE_TRIANGLES, DRAW_MODE_TRIANGLES_STRIP, DRAW_MODE_TRIANGLES_FAN})\n  public @interface DrawMode {}\n  /** Triangle draw mode. */\n  public static final int DRAW_MODE_TRIANGLES = 0;\n  /** Triangle strip draw mode. */\n  public static final int DRAW_MODE_TRIANGLES_STRIP = 1;\n  /** Triangle fan draw mode. */\n  public static final int DRAW_MODE_TRIANGLES_FAN = 2;\n\n  /** Number of position coordinates per vertex. */\n  public static final int TEXTURE_COORDS_PER_VERTEX = 2;\n  /** Number of texture coordinates per vertex. */\n  public static final int POSITION_COORDS_PER_VERTEX = 3;\n\n  /**\n   * Generates a complete sphere equirectangular projection.\n   *\n   * @param stereoMode A {@link C.StereoMode} value.\n   */\n  public static Projection createEquirectangular(@C.StereoMode int stereoMode) {\n    return createEquirectangular(\n        /* radius= */ 50, // Should be large enough that there are no stereo artifacts.\n        /* latitudes= */ 36, // Should be large enough to prevent videos looking wavy.\n        /* longitudes= */ 72, // Should be large enough to prevent videos looking wavy.\n        /* verticalFovDegrees= */ 180,\n        /* horizontalFovDegrees= */ 360,\n        stereoMode);\n  }\n\n  /**\n   * Generates an equirectangular projection.\n   *\n   * @param radius Size of the sphere. Must be &gt; 0.\n   * @param latitudes Number of rows that make up the sphere. Must be &gt;= 1.\n   * @param longitudes Number of columns that make up the sphere. Must be &gt;= 1.\n   * @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in\n   *     (0, 180].\n   * @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be\n   *     in (0, 360].\n   * @param stereoMode A {@link C.StereoMode} value.\n   * @return an equirectangular projection.\n   */\n  public static Projection createEquirectangular(\n      float radius,\n      int latitudes,\n      int longitudes,\n      float verticalFovDegrees,\n      float horizontalFovDegrees,\n      @C.StereoMode int stereoMode) {\n    Assertions.checkArgument(radius > 0);\n    Assertions.checkArgument(latitudes >= 1);\n    Assertions.checkArgument(longitudes >= 1);\n    Assertions.checkArgument(verticalFovDegrees > 0 && verticalFovDegrees <= 180);\n    Assertions.checkArgument(horizontalFovDegrees > 0 && horizontalFovDegrees <= 360);\n\n    // Compute angular size in radians of each UV quad.\n    float verticalFovRads = (float) Math.toRadians(verticalFovDegrees);\n    float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees);\n    float quadHeightRads = verticalFovRads / latitudes;\n    float quadWidthRads = horizontalFovRads / longitudes;\n\n    // Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices.\n    int vertexCount = (2 * (longitudes + 1) + 2) * latitudes;\n    // Buffer to return.\n    float[] vertexData = new float[vertexCount * POSITION_COORDS_PER_VERTEX];\n    float[] textureData = new float[vertexCount * TEXTURE_COORDS_PER_VERTEX];\n\n    // Generate the data for the sphere which is a set of triangle strips representing each\n    // latitude band.\n    int vOffset = 0; // Offset into the vertexData array.\n    int tOffset = 0; // Offset into the textureData array.\n    // (i, j) represents a quad in the equirectangular sphere.\n    for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip.\n      // Each latitude band lies between the two phi values. Each vertical edge on a band lies on\n      // a theta value.\n      float phiLow = quadHeightRads * j - verticalFovRads / 2;\n      float phiHigh = quadHeightRads * (j + 1) - verticalFovRads / 2;\n\n      for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band.\n        for (int k = 0; k < 2; ++k) { // For low and high points on an edge.\n          // For each point, determine it's position in polar coordinates.\n          float phi = k == 0 ? phiLow : phiHigh;\n          float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2;\n\n          // Set vertex position data as Cartesian coordinates.\n          vertexData[vOffset++] = -(float) (radius * Math.sin(theta) * Math.cos(phi));\n          vertexData[vOffset++] = (float) (radius * Math.sin(phi));\n          vertexData[vOffset++] = (float) (radius * Math.cos(theta) * Math.cos(phi));\n\n          textureData[tOffset++] = i * quadWidthRads / horizontalFovRads;\n          textureData[tOffset++] = (j + k) * quadHeightRads / verticalFovRads;\n\n          // Break up the triangle strip with degenerate vertices by copying first and last points.\n          if ((i == 0 && k == 0) || (i == longitudes && k == 1)) {\n            System.arraycopy(\n                vertexData,\n                vOffset - POSITION_COORDS_PER_VERTEX,\n                vertexData,\n                vOffset,\n                POSITION_COORDS_PER_VERTEX);\n            vOffset += POSITION_COORDS_PER_VERTEX;\n            System.arraycopy(\n                textureData,\n                tOffset - TEXTURE_COORDS_PER_VERTEX,\n                textureData,\n                tOffset,\n                TEXTURE_COORDS_PER_VERTEX);\n            tOffset += TEXTURE_COORDS_PER_VERTEX;\n          }\n        }\n        // Move on to the next vertical edge in the triangle strip.\n      }\n      // Move on to the next triangle strip.\n    }\n    SubMesh subMesh =\n        new SubMesh(SubMesh.VIDEO_TEXTURE_ID, vertexData, textureData, DRAW_MODE_TRIANGLES_STRIP);\n    return new Projection(new Mesh(subMesh), stereoMode);\n  }\n\n  /** The Mesh corresponding to the left eye. */\n  public final Mesh leftMesh;\n  /**\n   * The Mesh corresponding to the right eye. If {@code singleMesh} is true then this mesh is\n   * identical to {@link #leftMesh}.\n   */\n  public final Mesh rightMesh;\n  /** The stereo mode. */\n  public final @StereoMode int stereoMode;\n  /** Whether the left and right mesh are identical. */\n  public final boolean singleMesh;\n\n  /**\n   * Creates a Projection with single mesh.\n   *\n   * @param mesh the Mesh for both eyes.\n   * @param stereoMode A {@link StereoMode} value.\n   */\n  public Projection(Mesh mesh, int stereoMode) {\n    this(mesh, mesh, stereoMode);\n  }\n\n  /**\n   * Creates a Projection with dual mesh. Use {@link #Projection(Mesh, int)} if there is single mesh\n   * for both eyes.\n   *\n   * @param leftMesh the Mesh corresponding to the left eye.\n   * @param rightMesh the Mesh corresponding to the right eye.\n   * @param stereoMode A {@link C.StereoMode} value.\n   */\n  public Projection(Mesh leftMesh, Mesh rightMesh, int stereoMode) {\n    this.leftMesh = leftMesh;\n    this.rightMesh = rightMesh;\n    this.stereoMode = stereoMode;\n    this.singleMesh = leftMesh == rightMesh;\n  }\n\n  /** The sub mesh associated with the {@link Mesh}. */\n  public static final class SubMesh {\n    /** Texture ID for video frames. */\n    public static final int VIDEO_TEXTURE_ID = 0;\n\n    /** Texture ID. */\n    public final int textureId;\n    /** The drawing mode. One of {@link DrawMode}. */\n    public final @DrawMode int mode;\n    /** The SubMesh vertices. */\n    public final float[] vertices;\n    /** The SubMesh texture coordinates. */\n    public final float[] textureCoords;\n\n    public SubMesh(int textureId, float[] vertices, float[] textureCoords, @DrawMode int mode) {\n      this.textureId = textureId;\n      Assertions.checkArgument(\n          vertices.length * (long) TEXTURE_COORDS_PER_VERTEX\n              == textureCoords.length * (long) POSITION_COORDS_PER_VERTEX);\n      this.vertices = vertices;\n      this.textureCoords = textureCoords;\n      this.mode = mode;\n    }\n\n    /** Returns the SubMesh vertex count. */\n    public int getVertexCount() {\n      return vertices.length / POSITION_COORDS_PER_VERTEX;\n    }\n  }\n\n  /** A Mesh associated with the projection scene. */\n  public static final class Mesh {\n    private final SubMesh[] subMeshes;\n\n    public Mesh(SubMesh... subMeshes) {\n      this.subMeshes = subMeshes;\n    }\n\n    /** Returns the number of sub meshes. */\n    public int getSubMeshCount() {\n      return subMeshes.length;\n    }\n\n    /** Returns the SubMesh for the given index. */\n    public SubMesh getSubMesh(int index) {\n      return subMeshes[index];\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\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 */\npackage com.google.android.exoplayer2.video.spherical;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.spherical.Projection.Mesh;\nimport com.google.android.exoplayer2.video.spherical.Projection.SubMesh;\nimport java.util.ArrayList;\nimport java.util.zip.Inflater;\n\n/**\n * A decoder for the projection mesh.\n *\n * <p>The mesh boxes parsed are described at <a\n * href=\"https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md\">\n * Spherical Video V2 RFC</a>.\n *\n * <p>The decoder does not perform CRC checks at the moment.\n */\npublic final class ProjectionDecoder {\n\n  private static final int TYPE_YTMP = 0x79746d70;\n  private static final int TYPE_MSHP = 0x6d736870;\n  private static final int TYPE_RAW = 0x72617720;\n  private static final int TYPE_DFL8 = 0x64666c38;\n  private static final int TYPE_MESH = 0x6d657368;\n  private static final int TYPE_PROJ = 0x70726f6a;\n\n  // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to\n  // exceed these limits.\n  private static final int MAX_COORDINATE_COUNT = 10000;\n  private static final int MAX_VERTEX_COUNT = 32 * 1000;\n  private static final int MAX_TRIANGLE_INDICES = 128 * 1000;\n\n  private ProjectionDecoder() {}\n\n  /*\n   * Decodes the projection data.\n   *\n   * @param projectionData The projection data.\n   * @param stereoMode A {@link C.StereoMode} value.\n   * @return The projection or null if the data can't be decoded.\n   */\n  public static @Nullable Projection decode(byte[] projectionData, @C.StereoMode int stereoMode) {\n    ParsableByteArray input = new ParsableByteArray(projectionData);\n    // MP4 containers include the proj box but webm containers do not.\n    // Both containers use mshp.\n    ArrayList<Mesh> meshes = null;\n    try {\n      meshes = isProj(input) ? parseProj(input) : parseMshp(input);\n    } catch (ArrayIndexOutOfBoundsException ignored) {\n      // Do nothing.\n    }\n    if (meshes == null) {\n      return null;\n    } else {\n      switch (meshes.size()) {\n        case 1:\n          return new Projection(meshes.get(0), stereoMode);\n        case 2:\n          return new Projection(meshes.get(0), meshes.get(1), stereoMode);\n        case 0:\n        default:\n          return null;\n      }\n    }\n  }\n\n  /** Returns true if the input contains a proj box. Indicates MP4 container. */\n  private static boolean isProj(ParsableByteArray input) {\n    input.skipBytes(4); // size\n    int type = input.readInt();\n    input.setPosition(0);\n    return type == TYPE_PROJ;\n  }\n\n  private static @Nullable ArrayList<Mesh> parseProj(ParsableByteArray input) {\n    input.skipBytes(8); // size and type.\n    int position = input.getPosition();\n    int limit = input.limit();\n    while (position < limit) {\n      int childEnd = position + input.readInt();\n      if (childEnd <= position || childEnd > limit) {\n        return null;\n      }\n      int childAtomType = input.readInt();\n      // Some early files named the atom ytmp rather than mshp.\n      if (childAtomType == TYPE_YTMP || childAtomType == TYPE_MSHP) {\n        input.setLimit(childEnd);\n        return parseMshp(input);\n      }\n      position = childEnd;\n      input.setPosition(position);\n    }\n    return null;\n  }\n\n  private static @Nullable ArrayList<Mesh> parseMshp(ParsableByteArray input) {\n    int version = input.readUnsignedByte();\n    if (version != 0) {\n      return null;\n    }\n    input.skipBytes(7); // flags + crc.\n    int encoding = input.readInt();\n    if (encoding == TYPE_DFL8) {\n      ParsableByteArray output = new ParsableByteArray();\n      Inflater inflater = new Inflater(true);\n      try {\n        if (!Util.inflate(input, output, inflater)) {\n          return null;\n        }\n      } finally {\n        inflater.end();\n      }\n      input = output;\n    } else if (encoding != TYPE_RAW) {\n      return null;\n    }\n    return parseRawMshpData(input);\n  }\n\n  /** Parses MSHP data after the encoding_four_cc field. */\n  private static @Nullable ArrayList<Mesh> parseRawMshpData(ParsableByteArray input) {\n    ArrayList<Mesh> meshes = new ArrayList<>();\n    int position = input.getPosition();\n    int limit = input.limit();\n    while (position < limit) {\n      int childEnd = position + input.readInt();\n      if (childEnd <= position || childEnd > limit) {\n        return null;\n      }\n      int childAtomType = input.readInt();\n      if (childAtomType == TYPE_MESH) {\n        Mesh mesh = parseMesh(input);\n        if (mesh == null) {\n          return null;\n        }\n        meshes.add(mesh);\n      }\n      position = childEnd;\n      input.setPosition(position);\n    }\n    return meshes;\n  }\n\n  private static @Nullable Mesh parseMesh(ParsableByteArray input) {\n    // Read the coordinates.\n    int coordinateCount = input.readInt();\n    if (coordinateCount > MAX_COORDINATE_COUNT) {\n      return null;\n    }\n    float[] coordinates = new float[coordinateCount];\n    for (int coordinate = 0; coordinate < coordinateCount; coordinate++) {\n      coordinates[coordinate] = input.readFloat();\n    }\n    // Read the vertices.\n    int vertexCount = input.readInt();\n    if (vertexCount > MAX_VERTEX_COUNT) {\n      return null;\n    }\n\n    final double log2 = Math.log(2.0);\n    int coordinateCountSizeBits = (int) Math.ceil(Math.log(2.0 * coordinateCount) / log2);\n\n    ParsableBitArray bitInput = new ParsableBitArray(input.data);\n    bitInput.setPosition(input.getPosition() * 8);\n    float[] vertices = new float[vertexCount * 5];\n    int[] coordinateIndices = new int[5];\n    int vertexIndex = 0;\n    for (int vertex = 0; vertex < vertexCount; vertex++) {\n      for (int i = 0; i < 5; i++) {\n        int coordinateIndex =\n            coordinateIndices[i] + decodeZigZag(bitInput.readBits(coordinateCountSizeBits));\n        if (coordinateIndex >= coordinateCount || coordinateIndex < 0) {\n          return null;\n        }\n        vertices[vertexIndex++] = coordinates[coordinateIndex];\n        coordinateIndices[i] = coordinateIndex;\n      }\n    }\n\n    // Pad to next byte boundary\n    bitInput.setPosition(((bitInput.getPosition() + 7) & ~7));\n\n    int subMeshCount = bitInput.readBits(32);\n    SubMesh[] subMeshes = new SubMesh[subMeshCount];\n    for (int i = 0; i < subMeshCount; i++) {\n      int textureId = bitInput.readBits(8);\n      int drawMode = bitInput.readBits(8);\n      int triangleIndexCount = bitInput.readBits(32);\n      if (triangleIndexCount > MAX_TRIANGLE_INDICES) {\n        return null;\n      }\n      int vertexCountSizeBits = (int) Math.ceil(Math.log(2.0 * vertexCount) / log2);\n      int index = 0;\n      float[] triangleVertices = new float[triangleIndexCount * 3];\n      float[] textureCoords = new float[triangleIndexCount * 2];\n      for (int counter = 0; counter < triangleIndexCount; counter++) {\n        index += decodeZigZag(bitInput.readBits(vertexCountSizeBits));\n        if (index < 0 || index >= vertexCount) {\n          return null;\n        }\n        triangleVertices[counter * 3] = vertices[index * 5];\n        triangleVertices[counter * 3 + 1] = vertices[index * 5 + 1];\n        triangleVertices[counter * 3 + 2] = vertices[index * 5 + 2];\n        textureCoords[counter * 2] = vertices[index * 5 + 3];\n        textureCoords[counter * 2 + 1] = vertices[index * 5 + 4];\n      }\n      subMeshes[i] = new SubMesh(textureId, triangleVertices, textureCoords, drawMode);\n    }\n    return new Mesh(subMeshes);\n  }\n\n  /**\n   * Decodes Zigzag encoding as described in\n   * https://developers.google.com/protocol-buffers/docs/encoding#signed-integers\n   */\n  private static int decodeZigZag(int n) {\n    return (n >> 1) ^ -(n & 1);\n  }\n}\n"
  },
  {
    "path": "exoplayer/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\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@NonNullApi\npackage com.google.android.exoplayer2.video.spherical;\n\nimport com.google.android.exoplayer2.util.NonNullApi;\n"
  },
  {
    "path": "exoplayer/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">exoplayerlib</string>\n</resources>\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Jan 29 19:14:22 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.4.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\nandroid.injected.testOnly=false\n\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "ijkplayer/.gitignore",
    "content": "/build\n/ijkplayer.iml\n"
  },
  {
    "path": "ijkplayer/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 27\n    buildToolsVersion \"27.0.2\"\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 27\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(path: ':base')\n}\n"
  },
  {
    "path": "ijkplayer/ijkplayer.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":ijkplayer\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":ijkplayer\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_8\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 27 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"module\" module-name=\"base\" />\n  </component>\n</module>"
  },
  {
    "path": "ijkplayer/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "ijkplayer/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.ijkplayerlib\" />\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/AbstractMediaPlayer.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport tv.danmaku.ijk.media.player.misc.IMediaDataSource;\n\n@SuppressWarnings(\"WeakerAccess\")\npublic abstract class AbstractMediaPlayer implements IMediaPlayer {\n    private OnPreparedListener mOnPreparedListener;\n    private OnCompletionListener mOnCompletionListener;\n    private OnBufferingUpdateListener mOnBufferingUpdateListener;\n    private OnSeekCompleteListener mOnSeekCompleteListener;\n    private OnVideoSizeChangedListener mOnVideoSizeChangedListener;\n    private OnVideoDarSizeChangedListener mOnVideoDarSizeChangedListener;\n    private OnErrorListener mOnErrorListener;\n    private OnInfoListener mOnInfoListener;\n    private OnTimedTextListener mOnTimedTextListener;\n\n    public final void setOnPreparedListener(OnPreparedListener listener) {\n        mOnPreparedListener = listener;\n    }\n\n    public final void setOnCompletionListener(OnCompletionListener listener) {\n        mOnCompletionListener = listener;\n    }\n\n    public final void setOnBufferingUpdateListener(\n            OnBufferingUpdateListener listener) {\n        mOnBufferingUpdateListener = listener;\n    }\n\n    public final void setOnSeekCompleteListener(OnSeekCompleteListener listener) {\n        mOnSeekCompleteListener = listener;\n    }\n\n    public final void setOnVideoSizeChangedListener(\n            OnVideoSizeChangedListener listener) {\n        mOnVideoSizeChangedListener = listener;\n    }\n\n    public final void setOnVideoDarSizeChangedListener(\n            OnVideoDarSizeChangedListener listener) {\n        mOnVideoDarSizeChangedListener = listener;\n    }\n\n    public final void setOnErrorListener(OnErrorListener listener) {\n        mOnErrorListener = listener;\n    }\n\n    public final void setOnInfoListener(OnInfoListener listener) {\n        mOnInfoListener = listener;\n    }\n\n    public final void setOnTimedTextListener(OnTimedTextListener listener) {\n        mOnTimedTextListener = listener;\n    }\n\n    public void resetListeners() {\n        mOnPreparedListener = null;\n        mOnBufferingUpdateListener = null;\n        mOnCompletionListener = null;\n        mOnSeekCompleteListener = null;\n        mOnVideoSizeChangedListener = null;\n        mOnVideoDarSizeChangedListener = null;\n        mOnErrorListener = null;\n        mOnInfoListener = null;\n        mOnTimedTextListener = null;\n    }\n\n    protected final void notifyOnPrepared() {\n        if (mOnPreparedListener != null)\n            mOnPreparedListener.onPrepared(this);\n    }\n\n    protected final void notifyOnCompletion() {\n        if (mOnCompletionListener != null)\n            mOnCompletionListener.onCompletion(this);\n    }\n\n    protected final void notifyOnBufferingUpdate(int percent) {\n        if (mOnBufferingUpdateListener != null)\n            mOnBufferingUpdateListener.onBufferingUpdate(this, percent);\n    }\n\n    protected final void notifyOnSeekComplete() {\n        if (mOnSeekCompleteListener != null)\n            mOnSeekCompleteListener.onSeekComplete(this);\n    }\n\n    protected final void notifyOnVideoSizeChanged(int width, int height,\n                                                  int sarNum, int sarDen) {\n        if (mOnVideoSizeChangedListener != null)\n            mOnVideoSizeChangedListener.onVideoSizeChanged(this, width, height,\n                    sarNum, sarDen);\n    }\n\n    protected final void notifyOnVideoDarSizeChanged(int width, int height,\n                                                  int sarNum, int sarDen,\n                                                  int darNum, int darDen) {\n        if (mOnVideoDarSizeChangedListener != null)\n            mOnVideoDarSizeChangedListener.onVideoSizeChanged(this, width, height,\n                    sarNum, sarDen, darNum, darDen);\n    }\n\n    protected final boolean notifyOnError(int what, int extra) {\n        return mOnErrorListener != null && mOnErrorListener.onError(this, what, extra);\n    }\n\n    protected final boolean notifyOnInfo(int what, int extra) {\n        return mOnInfoListener != null && mOnInfoListener.onInfo(this, what, extra);\n    }\n\n    protected final void notifyOnTimedText(IjkTimedText text) {\n        if (mOnTimedTextListener != null)\n            mOnTimedTextListener.onTimedText(this, text);\n    }\n\n    public void setDataSource(IMediaDataSource mediaDataSource) {\n        throw new UnsupportedOperationException();\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/AndroidMediaPlayer.java",
    "content": "/*\n * Copyright (C) 2006 Bilibili\n * Copyright (C) 2006 The Android Open Source Project\n * Copyright (C) 2013 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.media.MediaDataSource;\nimport android.media.MediaPlayer;\nimport android.media.TimedText;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.text.TextUtils;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.lang.ref.WeakReference;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.misc.AndroidTrackInfo;\nimport tv.danmaku.ijk.media.player.misc.IMediaDataSource;\nimport tv.danmaku.ijk.media.player.misc.ITrackInfo;\nimport tv.danmaku.ijk.media.player.pragma.DebugLog;\n\npublic class AndroidMediaPlayer extends AbstractMediaPlayer {\n    private final MediaPlayer mInternalMediaPlayer;\n    private final AndroidMediaPlayerListenerHolder mInternalListenerAdapter;\n    private String mDataSource;\n    private MediaDataSource mMediaDataSource;\n\n    private final Object mInitLock = new Object();\n    private boolean mIsReleased;\n\n    private static MediaInfo sMediaInfo;\n\n    public AndroidMediaPlayer() {\n        synchronized (mInitLock) {\n            mInternalMediaPlayer = new MediaPlayer();\n        }\n        mInternalMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);\n        mInternalListenerAdapter = new AndroidMediaPlayerListenerHolder(this);\n        attachInternalListeners();\n    }\n\n    public MediaPlayer getInternalMediaPlayer() {\n        return mInternalMediaPlayer;\n    }\n\n    @Override\n    public void setDisplay(SurfaceHolder sh) {\n        synchronized (mInitLock) {\n            if (!mIsReleased) {\n                mInternalMediaPlayer.setDisplay(sh);\n            }\n        }\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    @Override\n    public void setSurface(Surface surface) {\n        mInternalMediaPlayer.setSurface(surface);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mInternalMediaPlayer.setDataSource(context, uri);\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mInternalMediaPlayer.setDataSource(context, uri, headers);\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd)\n            throws IOException, IllegalArgumentException, IllegalStateException {\n        mInternalMediaPlayer.setDataSource(fd);\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException,\n            IllegalArgumentException, SecurityException, IllegalStateException {\n        mDataSource = path;\n\n        Uri uri = Uri.parse(path);\n        String scheme = uri.getScheme();\n        if (!TextUtils.isEmpty(scheme) && scheme.equalsIgnoreCase(\"file\")) {\n            mInternalMediaPlayer.setDataSource(uri.getPath());\n        } else {\n            mInternalMediaPlayer.setDataSource(path);\n        }\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    @Override\n    public void setDataSource(IMediaDataSource mediaDataSource) {\n        releaseMediaDataSource();\n\n        mMediaDataSource = new MediaDataSourceProxy(mediaDataSource);\n        mInternalMediaPlayer.setDataSource(mMediaDataSource);\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    private static class MediaDataSourceProxy extends MediaDataSource {\n        private final IMediaDataSource mMediaDataSource;\n\n        public MediaDataSourceProxy(IMediaDataSource mediaDataSource) {\n            mMediaDataSource = mediaDataSource;\n        }\n\n        @Override\n        public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {\n            return mMediaDataSource.readAt(position, buffer, offset, size);\n        }\n\n        @Override\n        public long getSize() throws IOException {\n            return mMediaDataSource.getSize();\n        }\n\n        @Override\n        public void close() throws IOException {\n            mMediaDataSource.close();\n        }\n    }\n\n    @Override\n    public String getDataSource() {\n        return mDataSource;\n    }\n\n    private void releaseMediaDataSource() {\n        if (mMediaDataSource != null) {\n            try {\n                mMediaDataSource.close();\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n            mMediaDataSource = null;\n        }\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        mInternalMediaPlayer.prepareAsync();\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mInternalMediaPlayer.start();\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mInternalMediaPlayer.stop();\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mInternalMediaPlayer.pause();\n    }\n\n    @Override\n    public void setScreenOnWhilePlaying(boolean screenOn) {\n        mInternalMediaPlayer.setScreenOnWhilePlaying(screenOn);\n    }\n\n    @Override\n    public ITrackInfo[] getTrackInfo() {\n        return AndroidTrackInfo.fromMediaPlayer(mInternalMediaPlayer);\n    }\n\n    @Override\n    public int getVideoWidth() {\n        return mInternalMediaPlayer.getVideoWidth();\n    }\n\n    @Override\n    public int getVideoHeight() {\n        return mInternalMediaPlayer.getVideoHeight();\n    }\n\n    @Override\n    public int getVideoSarNum() {\n        return 1;\n    }\n\n    @Override\n    public int getVideoSarDen() {\n        return 1;\n    }\n\n    @Override\n    public boolean isPlaying() {\n        try {\n            return mInternalMediaPlayer.isPlaying();\n        } catch (IllegalStateException e) {\n            DebugLog.printStackTrace(e);\n            return false;\n        }\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        mInternalMediaPlayer.seekTo((int) msec);\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        try {\n            return mInternalMediaPlayer.getCurrentPosition();\n        } catch (IllegalStateException e) {\n            DebugLog.printStackTrace(e);\n            return 0;\n        }\n    }\n\n    @Override\n    public long getDuration() {\n        try {\n            return mInternalMediaPlayer.getDuration();\n        } catch (IllegalStateException e) {\n            DebugLog.printStackTrace(e);\n            return 0;\n        }\n    }\n\n    @Override\n    public void release() {\n        mIsReleased = true;\n        mInternalMediaPlayer.release();\n        releaseMediaDataSource();\n        resetListeners();\n        attachInternalListeners();\n    }\n\n    @Override\n    public void reset() {\n        try {\n            mInternalMediaPlayer.reset();\n        } catch (IllegalStateException e) {\n            DebugLog.printStackTrace(e);\n        }\n        releaseMediaDataSource();\n        resetListeners();\n        attachInternalListeners();\n    }\n\n    @Override\n    public void setLooping(boolean looping) {\n        mInternalMediaPlayer.setLooping(looping);\n    }\n\n    @Override\n    public boolean isLooping() {\n        return mInternalMediaPlayer.isLooping();\n    }\n\n    @Override\n    public void setVolume(float leftVolume, float rightVolume) {\n        mInternalMediaPlayer.setVolume(leftVolume, rightVolume);\n    }\n\n    @Override\n    public int getAudioSessionId() {\n        return mInternalMediaPlayer.getAudioSessionId();\n    }\n\n    @Override\n    public MediaInfo getMediaInfo() {\n        if (sMediaInfo == null) {\n            MediaInfo module = new MediaInfo();\n\n            module.mVideoDecoder = \"android\";\n            module.mVideoDecoderImpl = \"HW\";\n\n            module.mAudioDecoder = \"android\";\n            module.mAudioDecoderImpl = \"HW\";\n\n            sMediaInfo = module;\n        }\n\n        return sMediaInfo;\n    }\n\n    @Override\n    public void setLogEnabled(boolean enable) {\n    }\n\n    @Override\n    public boolean isPlayable() {\n        return true;\n    }\n\n    /*--------------------\n     * misc\n     */\n    @Override\n    public void setWakeMode(Context context, int mode) {\n        mInternalMediaPlayer.setWakeMode(context, mode);\n    }\n\n    @Override\n    public void setAudioStreamType(int streamtype) {\n        mInternalMediaPlayer.setAudioStreamType(streamtype);\n    }\n\n    @Override\n    public void setKeepInBackground(boolean keepInBackground) {\n    }\n\n    /*--------------------\n     * Listeners adapter\n     */\n    private void attachInternalListeners() {\n        mInternalMediaPlayer.setOnPreparedListener(mInternalListenerAdapter);\n        mInternalMediaPlayer\n                .setOnBufferingUpdateListener(mInternalListenerAdapter);\n        mInternalMediaPlayer.setOnCompletionListener(mInternalListenerAdapter);\n        mInternalMediaPlayer\n                .setOnSeekCompleteListener(mInternalListenerAdapter);\n        mInternalMediaPlayer\n                .setOnVideoSizeChangedListener(mInternalListenerAdapter);\n        mInternalMediaPlayer.setOnErrorListener(mInternalListenerAdapter);\n        mInternalMediaPlayer.setOnInfoListener(mInternalListenerAdapter);\n        mInternalMediaPlayer.setOnTimedTextListener(mInternalListenerAdapter);\n    }\n\n    private class AndroidMediaPlayerListenerHolder implements\n            MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,\n            MediaPlayer.OnBufferingUpdateListener,\n            MediaPlayer.OnSeekCompleteListener,\n            MediaPlayer.OnVideoSizeChangedListener,\n            MediaPlayer.OnErrorListener, MediaPlayer.OnInfoListener,\n            MediaPlayer.OnTimedTextListener {\n        public final WeakReference<AndroidMediaPlayer> mWeakMediaPlayer;\n\n        public AndroidMediaPlayerListenerHolder(AndroidMediaPlayer mp) {\n            mWeakMediaPlayer = new WeakReference<AndroidMediaPlayer>(mp);\n        }\n\n        @Override\n        public boolean onInfo(MediaPlayer mp, int what, int extra) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            return self != null && notifyOnInfo(what, extra);\n\n        }\n\n        @Override\n        public boolean onError(MediaPlayer mp, int what, int extra) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            return self != null && notifyOnError(what, extra);\n\n        }\n\n        @Override\n        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            notifyOnVideoSizeChanged(width, height, 1, 1);\n        }\n\n        @Override\n        public void onSeekComplete(MediaPlayer mp) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            notifyOnSeekComplete();\n        }\n\n        @Override\n        public void onBufferingUpdate(MediaPlayer mp, int percent) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            notifyOnBufferingUpdate(percent);\n        }\n\n        @Override\n        public void onCompletion(MediaPlayer mp) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            notifyOnCompletion();\n        }\n\n        @Override\n        public void onPrepared(MediaPlayer mp) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            notifyOnPrepared();\n        }\n\n        @Override\n        public void onTimedText(MediaPlayer mp, TimedText text) {\n            AndroidMediaPlayer self = mWeakMediaPlayer.get();\n            if (self == null)\n                return;\n\n            IjkTimedText ijkText = null;\n\n            if (text != null) {\n                ijkText = new IjkTimedText(text.getBounds(), text.getText());\n            }\n\n            notifyOnTimedText(ijkText);\n        }\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IMediaPlayer.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.misc.IMediaDataSource;\nimport tv.danmaku.ijk.media.player.misc.ITrackInfo;\n\npublic interface IMediaPlayer {\n    /*\n     * Do not change these values without updating their counterparts in native\n     */\n    int MEDIA_INFO_UNKNOWN = 1;\n    int MEDIA_INFO_STARTED_AS_NEXT = 2;\n    int MEDIA_INFO_VIDEO_RENDERING_START = 3;\n    int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;\n    int MEDIA_INFO_BUFFERING_START = 701;\n    int MEDIA_INFO_BUFFERING_END = 702;\n    int MEDIA_INFO_NETWORK_BANDWIDTH = 703;\n    int MEDIA_INFO_BAD_INTERLEAVING = 800;\n    int MEDIA_INFO_NOT_SEEKABLE = 801;\n    int MEDIA_INFO_METADATA_UPDATE = 802;\n    int MEDIA_INFO_TIMED_TEXT_ERROR = 900;\n    int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;\n    int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;\n\n    int MEDIA_INFO_VIDEO_ROTATION_CHANGED = 10001;\n    int MEDIA_INFO_AUDIO_RENDERING_START  = 10002;\n    int MEDIA_INFO_AUDIO_DECODED_START    = 10003;\n    int MEDIA_INFO_VIDEO_DECODED_START    = 10004;\n    int MEDIA_INFO_OPEN_INPUT             = 10005;\n    int MEDIA_INFO_FIND_STREAM_INFO       = 10006;\n    int MEDIA_INFO_COMPONENT_OPEN         = 10007;\n    int MEDIA_INFO_VIDEO_SEEK_RENDERING_START = 10008;\n    int MEDIA_INFO_AUDIO_SEEK_RENDERING_START = 10009;\n    int MEDIA_INFO_MEDIA_ACCURATE_SEEK_COMPLETE = 10100;\n\n    int MEDIA_ERROR_UNKNOWN = 1;\n    int MEDIA_ERROR_SERVER_DIED = 100;\n    int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;\n    int MEDIA_ERROR_IO = -1004;\n    int MEDIA_ERROR_MALFORMED = -1007;\n    int MEDIA_ERROR_UNSUPPORTED = -1010;\n    int MEDIA_ERROR_TIMED_OUT = -110;\n\n    void setDisplay(SurfaceHolder sh);\n\n    void setDataSource(Context context, Uri uri)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    void setDataSource(Context context, Uri uri, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    void setDataSource(FileDescriptor fd)\n            throws IOException, IllegalArgumentException, IllegalStateException;\n\n    void setDataSource(String path)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    String getDataSource();\n\n    void prepareAsync() throws IllegalStateException;\n\n    void start() throws IllegalStateException;\n\n    void stop() throws IllegalStateException;\n\n    void pause() throws IllegalStateException;\n\n    void setScreenOnWhilePlaying(boolean screenOn);\n\n    int getVideoWidth();\n\n    int getVideoHeight();\n\n    boolean isPlaying();\n\n    void seekTo(long msec) throws IllegalStateException;\n\n    long getCurrentPosition();\n\n    long getDuration();\n\n    void release();\n\n    void reset();\n\n    void setVolume(float leftVolume, float rightVolume);\n\n    int getAudioSessionId();\n\n    MediaInfo getMediaInfo();\n\n    @SuppressWarnings(\"EmptyMethod\")\n    @Deprecated\n    void setLogEnabled(boolean enable);\n\n    @Deprecated\n    boolean isPlayable();\n\n    void setOnPreparedListener(OnPreparedListener listener);\n\n    void setOnCompletionListener(OnCompletionListener listener);\n\n    void setOnBufferingUpdateListener(\n            OnBufferingUpdateListener listener);\n\n    void setOnSeekCompleteListener(\n            OnSeekCompleteListener listener);\n\n    void setOnVideoSizeChangedListener(\n            OnVideoSizeChangedListener listener);\n\n    void setOnErrorListener(OnErrorListener listener);\n\n    void setOnInfoListener(OnInfoListener listener);\n\n    void setOnTimedTextListener(OnTimedTextListener listener);\n\n    /*--------------------\n     * Listeners\n     */\n    interface OnPreparedListener {\n        void onPrepared(IMediaPlayer mp);\n    }\n\n    interface OnCompletionListener {\n        void onCompletion(IMediaPlayer mp);\n    }\n\n    interface OnBufferingUpdateListener {\n        void onBufferingUpdate(IMediaPlayer mp, int percent);\n    }\n\n    interface OnSeekCompleteListener {\n        void onSeekComplete(IMediaPlayer mp);\n    }\n\n    interface OnVideoSizeChangedListener {\n        void onVideoSizeChanged(IMediaPlayer mp, int width, int height,\n                                int sar_num, int sar_den);\n    }\n\n    interface OnVideoDarSizeChangedListener {\n        void onVideoSizeChanged(IMediaPlayer mp, int width, int height,\n                                int sar_num, int sar_den, int dar_num, int dar_den);\n    }\n\n    interface OnErrorListener {\n        boolean onError(IMediaPlayer mp, int what, int extra);\n    }\n\n    interface OnInfoListener {\n        boolean onInfo(IMediaPlayer mp, int what, int extra);\n    }\n\n    interface OnTimedTextListener {\n        void onTimedText(IMediaPlayer mp, IjkTimedText text);\n    }\n\n    /*--------------------\n     * Optional\n     */\n    void setAudioStreamType(int streamtype);\n\n    @Deprecated\n    void setKeepInBackground(boolean keepInBackground);\n\n    int getVideoSarNum();\n\n    int getVideoSarDen();\n\n    @Deprecated\n    void setWakeMode(Context context, int mode);\n\n    void setLooping(boolean looping);\n\n    boolean isLooping();\n\n    /*--------------------\n     * AndroidMediaPlayer: JELLY_BEAN\n     */\n    ITrackInfo[] getTrackInfo();\n\n    /*--------------------\n     * AndroidMediaPlayer: ICE_CREAM_SANDWICH:\n     */\n    void setSurface(Surface surface);\n\n    /*--------------------\n     * AndroidMediaPlayer: M:\n     */\n    void setDataSource(IMediaDataSource mediaDataSource);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/ISurfaceTextureHolder.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.graphics.SurfaceTexture;\n\npublic interface ISurfaceTextureHolder {\n    void setSurfaceTexture(SurfaceTexture surfaceTexture);\n\n    SurfaceTexture getSurfaceTexture();\n\n    void setSurfaceTextureHost(ISurfaceTextureHost surfaceTextureHost);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/ISurfaceTextureHost.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.graphics.SurfaceTexture;\n\npublic interface ISurfaceTextureHost {\n    void releaseSurfaceTexture(SurfaceTexture surfaceTexture);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IjkLibLoader.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\npublic interface IjkLibLoader {\n    void loadLibrary(String libName) throws UnsatisfiedLinkError,\n            SecurityException;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IjkMediaCodecInfo.java",
    "content": "package tv.danmaku.ijk.media.player;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaCodecInfo;\nimport android.media.MediaCodecInfo.CodecCapabilities;\nimport android.media.MediaCodecInfo.CodecProfileLevel;\nimport android.os.Build;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.TreeMap;\n\npublic class IjkMediaCodecInfo {\n    private final static String TAG = \"IjkMediaCodecInfo\";\n\n    public static final int RANK_MAX = 1000;\n    public static final int RANK_TESTED = 800;\n    public static final int RANK_ACCEPTABLE = 700;\n    public static final int RANK_LAST_CHANCE = 600;\n    public static final int RANK_SECURE = 300;\n    public static final int RANK_SOFTWARE = 200;\n    public static final int RANK_NON_STANDARD = 100;\n    public static final int RANK_NO_SENSE = 0;\n\n    public MediaCodecInfo mCodecInfo;\n    public int mRank = 0;\n    public String mMimeType;\n\n    private static Map<String, Integer> sKnownCodecList;\n\n    private static synchronized Map<String, Integer> getKnownCodecList() {\n        if (sKnownCodecList != null)\n            return sKnownCodecList;\n\n        sKnownCodecList = new TreeMap<String, Integer>(\n                String.CASE_INSENSITIVE_ORDER);\n\n        // ----- Nvidia -----\n        // Tegra3\n        // Nexus 7 (2012)\n        // Tegra K1\n        // Nexus 9\n        sKnownCodecList.put(\"OMX.Nvidia.h264.decode\", RANK_TESTED);\n        sKnownCodecList.put(\"OMX.Nvidia.h264.decode.secure\", RANK_SECURE);\n\n        // ----- Intel -----\n        // Atom Z3735\n        // Teclast X98 Air\n        sKnownCodecList.put(\"OMX.Intel.hw_vd.h264\", RANK_TESTED + 1);\n        // Atom Z2560\n        // Dell Venue 7 3730\n        sKnownCodecList.put(\"OMX.Intel.VideoDecoder.AVC\", RANK_TESTED);\n\n        // ----- Qualcomm -----\n        // MSM8260\n        // Xiaomi MI 1S\n        sKnownCodecList.put(\"OMX.qcom.video.decoder.avc\", RANK_TESTED);\n        sKnownCodecList.put(\"OMX.ittiam.video.decoder.avc\", RANK_NO_SENSE);\n\n        // ----- Samsung -----\n        // Exynos 3110\n        // Nexus S\n        sKnownCodecList.put(\"OMX.SEC.avc.dec\", RANK_TESTED);\n        sKnownCodecList.put(\"OMX.SEC.AVC.Decoder\", RANK_TESTED - 1);\n        // OMX.SEC.avcdec doesn't reorder output pictures on GT-9100\n        sKnownCodecList.put(\"OMX.SEC.avcdec\", RANK_TESTED - 2);\n        sKnownCodecList.put(\"OMX.SEC.avc.sw.dec\", RANK_SOFTWARE);\n        // Exynos 5 ?\n        sKnownCodecList.put(\"OMX.Exynos.avc.dec\", RANK_TESTED);\n        sKnownCodecList.put(\"OMX.Exynos.AVC.Decoder\", RANK_TESTED - 1);\n\n        // ------ Huawei hisilicon ------\n        // Kirin 910, Mali 450 MP\n        // Huawei HONOR 3C (H30-L01)\n        sKnownCodecList.put(\"OMX.k3.video.decoder.avc\", RANK_TESTED);\n        // Kirin 920, Mali T624\n        // Huawei HONOR 6\n        sKnownCodecList.put(\"OMX.IMG.MSVDX.Decoder.AVC\", RANK_TESTED);\n\n        // ----- TI -----\n        // TI OMAP4460\n        // Galaxy Nexus\n        sKnownCodecList.put(\"OMX.TI.DUCATI1.VIDEO.DECODER\", RANK_TESTED);\n\n        // ------ RockChip ------\n        // Youku TVBox\n        sKnownCodecList.put(\"OMX.rk.video_decoder.avc\", RANK_TESTED);\n\n        // ------ AMLogic -----\n        // MiBox1, 1s, 2\n        sKnownCodecList.put(\"OMX.amlogic.avc.decoder.awesome\", RANK_TESTED);\n\n        // ------ Marvell ------\n        // Lenovo A788t\n        sKnownCodecList.put(\"OMX.MARVELL.VIDEO.HW.CODA7542DECODER\", RANK_TESTED);\n        sKnownCodecList.put(\"OMX.MARVELL.VIDEO.H264DECODER\", RANK_SOFTWARE);\n\n        // ----- TODO: need test -----\n        sKnownCodecList.remove(\"OMX.Action.Video.Decoder\");\n        sKnownCodecList.remove(\"OMX.allwinner.video.decoder.avc\");\n        sKnownCodecList.remove(\"OMX.BRCM.vc4.decoder.avc\");\n        sKnownCodecList.remove(\"OMX.brcm.video.h264.hw.decoder\");\n        sKnownCodecList.remove(\"OMX.brcm.video.h264.decoder\");\n        sKnownCodecList.remove(\"OMX.cosmo.video.decoder.avc\");\n        sKnownCodecList.remove(\"OMX.duos.h264.decoder\");\n        sKnownCodecList.remove(\"OMX.hantro.81x0.video.decoder\");\n        sKnownCodecList.remove(\"OMX.hantro.G1.video.decoder\");\n        sKnownCodecList.remove(\"OMX.hisi.video.decoder\");\n        sKnownCodecList.remove(\"OMX.LG.decoder.video.avc\");\n        sKnownCodecList.remove(\"OMX.MS.AVC.Decoder\");\n        sKnownCodecList.remove(\"OMX.RENESAS.VIDEO.DECODER.H264\");\n        sKnownCodecList.remove(\"OMX.RTK.video.decoder\");\n        sKnownCodecList.remove(\"OMX.sprd.h264.decoder\");\n        sKnownCodecList.remove(\"OMX.ST.VFM.H264Dec\");\n        sKnownCodecList.remove(\"OMX.vpu.video_decoder.avc\");\n        sKnownCodecList.remove(\"OMX.WMT.decoder.avc\");\n\n        // Really ?\n        sKnownCodecList.remove(\"OMX.bluestacks.hw.decoder\");\n\n        // ---------------\n        // Useless codec\n        // ----- google -----\n        sKnownCodecList.put(\"OMX.google.h264.decoder\", RANK_SOFTWARE);\n        sKnownCodecList.put(\"OMX.google.h264.lc.decoder\", RANK_SOFTWARE);\n        // ----- huawei k920 -----\n        sKnownCodecList.put(\"OMX.k3.ffmpeg.decoder\", RANK_SOFTWARE);\n        sKnownCodecList.put(\"OMX.ffmpeg.video.decoder\", RANK_SOFTWARE);\n        // ----- unknown -----\n        sKnownCodecList.put(\"OMX.sprd.soft.h264.decoder\", RANK_SOFTWARE);\n\n        return sKnownCodecList;\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    public static IjkMediaCodecInfo setupCandidate(MediaCodecInfo codecInfo,\n            String mimeType) {\n        if (codecInfo == null\n                || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)\n            return null;\n\n        String name = codecInfo.getName();\n        if (TextUtils.isEmpty(name))\n            return null;\n\n        name = name.toLowerCase(Locale.US);\n        int rank = RANK_NO_SENSE;\n        if (!name.startsWith(\"omx.\")) {\n            rank = RANK_NON_STANDARD;\n        } else if (name.startsWith(\"omx.pv\")) {\n            rank = RANK_SOFTWARE;\n        } else if (name.startsWith(\"omx.google.\")) {\n            rank = RANK_SOFTWARE;\n        } else if (name.startsWith(\"omx.ffmpeg.\")) {\n            rank = RANK_SOFTWARE;\n        } else if (name.startsWith(\"omx.k3.ffmpeg.\")) {\n            rank = RANK_SOFTWARE;\n        } else if (name.startsWith(\"omx.avcodec.\")) {\n            rank = RANK_SOFTWARE;\n        } else if (name.startsWith(\"omx.ittiam.\")) {\n            // unknown codec in qualcomm SoC\n            rank = RANK_NO_SENSE;\n        } else if (name.startsWith(\"omx.mtk.\")) {\n            // 1. MTK only works on 4.3 and above\n            // 2. MTK works on MIUI 6 (4.2.1)\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)\n                rank = RANK_NO_SENSE;\n            else\n                rank = RANK_TESTED;\n        } else {\n            Integer knownRank = getKnownCodecList().get(name);\n            if (knownRank != null) {\n                rank = knownRank;\n            } else {\n                try {\n                    CodecCapabilities cap = codecInfo\n                            .getCapabilitiesForType(mimeType);\n                    if (cap != null)\n                        rank = RANK_ACCEPTABLE;\n                    else\n                        rank = RANK_LAST_CHANCE;\n                } catch (Throwable e) {\n                    rank = RANK_LAST_CHANCE;\n                }\n            }\n        }\n\n        IjkMediaCodecInfo candidate = new IjkMediaCodecInfo();\n        candidate.mCodecInfo = codecInfo;\n        candidate.mRank = rank;\n        candidate.mMimeType = mimeType;\n        return candidate;\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    public void dumpProfileLevels(String mimeType) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)\n            return;\n\n        try {\n            CodecCapabilities caps = mCodecInfo\n                    .getCapabilitiesForType(mimeType);\n            int maxProfile = 0;\n            int maxLevel = 0;\n            if (caps != null) {\n                if (caps.profileLevels != null) {\n                    for (CodecProfileLevel profileLevel : caps.profileLevels) {\n                        if (profileLevel == null)\n                            continue;\n\n                        maxProfile = Math.max(maxProfile, profileLevel.profile);\n                        maxLevel = Math.max(maxLevel, profileLevel.level);\n                    }\n                }\n            }\n\n            Log.i(TAG,\n                    String.format(Locale.US, \"%s\",\n                            getProfileLevelName(maxProfile, maxLevel)));\n        } catch (Throwable e) {\n            Log.i(TAG, \"profile-level: exception\");\n        }\n    }\n\n    public static String getProfileLevelName(int profile, int level) {\n        return String.format(Locale.US, \" %s Profile Level %s (%d,%d)\",\n                getProfileName(profile), getLevelName(level), profile, level);\n    }\n\n    public static String getProfileName(int profile) {\n        switch (profile) {\n        case CodecProfileLevel.AVCProfileBaseline:\n            return \"Baseline\";\n        case CodecProfileLevel.AVCProfileMain:\n            return \"Main\";\n        case CodecProfileLevel.AVCProfileExtended:\n            return \"Extends\";\n        case CodecProfileLevel.AVCProfileHigh:\n            return \"High\";\n        case CodecProfileLevel.AVCProfileHigh10:\n            return \"High10\";\n        case CodecProfileLevel.AVCProfileHigh422:\n            return \"High422\";\n        case CodecProfileLevel.AVCProfileHigh444:\n            return \"High444\";\n        default:\n            return \"Unknown\";\n        }\n    }\n\n    public static String getLevelName(int level) {\n        switch (level) {\n        case CodecProfileLevel.AVCLevel1:\n            return \"1\";\n        case CodecProfileLevel.AVCLevel1b:\n            return \"1b\";\n        case CodecProfileLevel.AVCLevel11:\n            return \"11\";\n        case CodecProfileLevel.AVCLevel12:\n            return \"12\";\n        case CodecProfileLevel.AVCLevel13:\n            return \"13\";\n        case CodecProfileLevel.AVCLevel2:\n            return \"2\";\n        case CodecProfileLevel.AVCLevel21:\n            return \"21\";\n        case CodecProfileLevel.AVCLevel22:\n            return \"22\";\n        case CodecProfileLevel.AVCLevel3:\n            return \"3\";\n        case CodecProfileLevel.AVCLevel31:\n            return \"31\";\n        case CodecProfileLevel.AVCLevel32:\n            return \"32\";\n        case CodecProfileLevel.AVCLevel4:\n            return \"4\";\n        case CodecProfileLevel.AVCLevel41:\n            return \"41\";\n        case CodecProfileLevel.AVCLevel42:\n            return \"42\";\n        case CodecProfileLevel.AVCLevel5:\n            return \"5\";\n        case CodecProfileLevel.AVCLevel51:\n            return \"51\";\n        case 65536: // CodecProfileLevel.AVCLevel52:\n            return \"52\";\n        default:\n            return \"0\";\n        }\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IjkMediaMeta.java",
    "content": "package tv.danmaku.ijk.media.player;\n\nimport android.os.Bundle;\nimport android.text.TextUtils;\n\nimport java.util.ArrayList;\nimport java.util.Locale;\n\n@SuppressWarnings(\"SameParameterValue\")\npublic class IjkMediaMeta {\n    // media meta\n    public static final String IJKM_KEY_FORMAT = \"format\";\n    public static final String IJKM_KEY_DURATION_US = \"duration_us\";\n    public static final String IJKM_KEY_START_US = \"start_us\";\n    public static final String IJKM_KEY_BITRATE = \"bitrate\";\n    public static final String IJKM_KEY_VIDEO_STREAM = \"video\";\n    public static final String IJKM_KEY_AUDIO_STREAM = \"audio\";\n    public static final String IJKM_KEY_TIMEDTEXT_STREAM = \"timedtext\";\n\n    // stream meta\n    public static final String IJKM_KEY_TYPE = \"type\";\n    public static final String IJKM_VAL_TYPE__VIDEO = \"video\";\n    public static final String IJKM_VAL_TYPE__AUDIO = \"audio\";\n    public static final String IJKM_VAL_TYPE__TIMEDTEXT = \"timedtext\";\n    public static final String IJKM_VAL_TYPE__UNKNOWN = \"unknown\";\n    public static final String IJKM_KEY_LANGUAGE = \"language\";\n\n    public static final String IJKM_KEY_CODEC_NAME = \"codec_name\";\n    public static final String IJKM_KEY_CODEC_PROFILE = \"codec_profile\";\n    public static final String IJKM_KEY_CODEC_LEVEL = \"codec_level\";\n    public static final String IJKM_KEY_CODEC_LONG_NAME = \"codec_long_name\";\n    public static final String IJKM_KEY_CODEC_PIXEL_FORMAT = \"codec_pixel_format\";\n    public static final String IJKM_KEY_CODEC_PROFILE_ID = \"codec_profile_id\";\n\n    // stream: video\n    public static final String IJKM_KEY_WIDTH = \"width\";\n    public static final String IJKM_KEY_HEIGHT = \"height\";\n    public static final String IJKM_KEY_FPS_NUM = \"fps_num\";\n    public static final String IJKM_KEY_FPS_DEN = \"fps_den\";\n    public static final String IJKM_KEY_TBR_NUM = \"tbr_num\";\n    public static final String IJKM_KEY_TBR_DEN = \"tbr_den\";\n    public static final String IJKM_KEY_SAR_NUM = \"sar_num\";\n    public static final String IJKM_KEY_SAR_DEN = \"sar_den\";\n    // stream: audio\n    public static final String IJKM_KEY_SAMPLE_RATE = \"sample_rate\";\n    public static final String IJKM_KEY_CHANNEL_LAYOUT = \"channel_layout\";\n\n    public static final String IJKM_KEY_STREAMS = \"streams\";\n\n    public static final long AV_CH_FRONT_LEFT = 0x00000001;\n    public static final long AV_CH_FRONT_RIGHT = 0x00000002;\n    public static final long AV_CH_FRONT_CENTER = 0x00000004;\n    public static final long AV_CH_LOW_FREQUENCY = 0x00000008;\n    public static final long AV_CH_BACK_LEFT = 0x00000010;\n    public static final long AV_CH_BACK_RIGHT = 0x00000020;\n    public static final long AV_CH_FRONT_LEFT_OF_CENTER = 0x00000040;\n    public static final long AV_CH_FRONT_RIGHT_OF_CENTER = 0x00000080;\n    public static final long AV_CH_BACK_CENTER = 0x00000100;\n    public static final long AV_CH_SIDE_LEFT = 0x00000200;\n    public static final long AV_CH_SIDE_RIGHT = 0x00000400;\n    public static final long AV_CH_TOP_CENTER = 0x00000800;\n    public static final long AV_CH_TOP_FRONT_LEFT = 0x00001000;\n    public static final long AV_CH_TOP_FRONT_CENTER = 0x00002000;\n    public static final long AV_CH_TOP_FRONT_RIGHT = 0x00004000;\n    public static final long AV_CH_TOP_BACK_LEFT = 0x00008000;\n    public static final long AV_CH_TOP_BACK_CENTER = 0x00010000;\n    public static final long AV_CH_TOP_BACK_RIGHT = 0x00020000;\n    public static final long AV_CH_STEREO_LEFT = 0x20000000;\n    public static final long AV_CH_STEREO_RIGHT = 0x40000000;\n    public static final long AV_CH_WIDE_LEFT = 0x0000000080000000L;\n    public static final long AV_CH_WIDE_RIGHT = 0x0000000100000000L;\n    public static final long AV_CH_SURROUND_DIRECT_LEFT = 0x0000000200000000L;\n    public static final long AV_CH_SURROUND_DIRECT_RIGHT = 0x0000000400000000L;\n    public static final long AV_CH_LOW_FREQUENCY_2 = 0x0000000800000000L;\n\n    public static final long AV_CH_LAYOUT_MONO = (AV_CH_FRONT_CENTER);\n    public static final long AV_CH_LAYOUT_STEREO = (AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT);\n    public static final long AV_CH_LAYOUT_2POINT1 = (AV_CH_LAYOUT_STEREO | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_2_1 = (AV_CH_LAYOUT_STEREO | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_SURROUND = (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER);\n    public static final long AV_CH_LAYOUT_3POINT1 = (AV_CH_LAYOUT_SURROUND | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_4POINT0 = (AV_CH_LAYOUT_SURROUND | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_4POINT1 = (AV_CH_LAYOUT_4POINT0 | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_2_2 = (AV_CH_LAYOUT_STEREO\n            | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT);\n    public static final long AV_CH_LAYOUT_QUAD = (AV_CH_LAYOUT_STEREO\n            | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT);\n    public static final long AV_CH_LAYOUT_5POINT0 = (AV_CH_LAYOUT_SURROUND\n            | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT);\n    public static final long AV_CH_LAYOUT_5POINT1 = (AV_CH_LAYOUT_5POINT0 | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_5POINT0_BACK = (AV_CH_LAYOUT_SURROUND\n            | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT);\n    public static final long AV_CH_LAYOUT_5POINT1_BACK = (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_6POINT0 = (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_6POINT0_FRONT = (AV_CH_LAYOUT_2_2\n            | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER);\n    public static final long AV_CH_LAYOUT_HEXAGONAL = (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_6POINT1 = (AV_CH_LAYOUT_5POINT1 | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_6POINT1_BACK = (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_BACK_CENTER);\n    public static final long AV_CH_LAYOUT_6POINT1_FRONT = (AV_CH_LAYOUT_6POINT0_FRONT | AV_CH_LOW_FREQUENCY);\n    public static final long AV_CH_LAYOUT_7POINT0 = (AV_CH_LAYOUT_5POINT0\n            | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT);\n    public static final long AV_CH_LAYOUT_7POINT0_FRONT = (AV_CH_LAYOUT_5POINT0\n            | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER);\n    public static final long AV_CH_LAYOUT_7POINT1 = (AV_CH_LAYOUT_5POINT1\n            | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT);\n    public static final long AV_CH_LAYOUT_7POINT1_WIDE = (AV_CH_LAYOUT_5POINT1\n            | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER);\n    public static final long AV_CH_LAYOUT_7POINT1_WIDE_BACK = (AV_CH_LAYOUT_5POINT1_BACK\n            | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER);\n    public static final long AV_CH_LAYOUT_OCTAGONAL = (AV_CH_LAYOUT_5POINT0\n            | AV_CH_BACK_LEFT | AV_CH_BACK_CENTER | AV_CH_BACK_RIGHT);\n    public static final long AV_CH_LAYOUT_STEREO_DOWNMIX = (AV_CH_STEREO_LEFT | AV_CH_STEREO_RIGHT);\n\n    public static final int FF_PROFILE_H264_CONSTRAINED = (1<<9);  // 8+1; constraint_set1_flag\n    public static final int FF_PROFILE_H264_INTRA = (1<<11);       // 8+3; constraint_set3_flag\n\n    public static final int FF_PROFILE_H264_BASELINE = 66;\n    public static final int FF_PROFILE_H264_CONSTRAINED_BASELINE = (66|FF_PROFILE_H264_CONSTRAINED);\n    public static final int FF_PROFILE_H264_MAIN = 77;\n    public static final int FF_PROFILE_H264_EXTENDED = 88;\n    public static final int FF_PROFILE_H264_HIGH = 100;\n    public static final int FF_PROFILE_H264_HIGH_10 = 110;\n    public static final int FF_PROFILE_H264_HIGH_10_INTRA = (110|FF_PROFILE_H264_INTRA);\n    public static final int FF_PROFILE_H264_HIGH_422 = 122;\n    public static final int FF_PROFILE_H264_HIGH_422_INTRA = (122|FF_PROFILE_H264_INTRA);\n    public static final int FF_PROFILE_H264_HIGH_444 = 144;\n    public static final int FF_PROFILE_H264_HIGH_444_PREDICTIVE = 244;\n    public static final int FF_PROFILE_H264_HIGH_444_INTRA = (244|FF_PROFILE_H264_INTRA);\n    public static final int FF_PROFILE_H264_CAVLC_444 = 44;\n\n    public Bundle mMediaMeta;\n\n    public String mFormat;\n    public long mDurationUS;\n    public long mStartUS;\n    public long mBitrate;\n\n    public final ArrayList<IjkStreamMeta> mStreams = new ArrayList<IjkStreamMeta>();\n    public IjkStreamMeta mVideoStream;\n    public IjkStreamMeta mAudioStream;\n\n    public String getString(String key) {\n        return mMediaMeta.getString(key);\n    }\n\n    public int getInt(String key) {\n        return getInt(key, 0);\n    }\n\n    public int getInt(String key, int defaultValue) {\n        String value = getString(key);\n        if (TextUtils.isEmpty(value))\n            return defaultValue;\n\n        try {\n            return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n            return defaultValue;\n        }\n    }\n\n    public long getLong(String key) {\n        return getLong(key, 0);\n    }\n\n    public long getLong(String key, long defaultValue) {\n        String value = getString(key);\n        if (TextUtils.isEmpty(value))\n            return defaultValue;\n\n        try {\n            return Long.parseLong(value);\n        } catch (NumberFormatException e) {\n            return defaultValue;\n        }\n    }\n\n    public ArrayList<Bundle> getParcelableArrayList(String key) {\n        return mMediaMeta.getParcelableArrayList(key);\n    }\n\n    public String getDurationInline() {\n        long duration = mDurationUS + 5000;\n        long secs = duration / 1000000;\n        long mins = secs / 60;\n        secs %= 60;\n        long hours = mins / 60;\n        mins %= 60;\n        return String.format(Locale.US, \"%02d:%02d:%02d\", hours, mins, secs);\n    }\n\n    public static IjkMediaMeta parse(Bundle mediaMeta) {\n        if (mediaMeta == null)\n            return null;\n\n        IjkMediaMeta meta = new IjkMediaMeta();\n        meta.mMediaMeta = mediaMeta;\n\n        meta.mFormat = meta.getString(IJKM_KEY_FORMAT);\n        meta.mDurationUS = meta.getLong(IJKM_KEY_DURATION_US);\n        meta.mStartUS = meta.getLong(IJKM_KEY_START_US);\n        meta.mBitrate = meta.getLong(IJKM_KEY_BITRATE);\n\n        int videoStreamIndex = meta.getInt(IJKM_KEY_VIDEO_STREAM, -1);\n        int audioStreamIndex = meta.getInt(IJKM_KEY_AUDIO_STREAM, -1);\n        int subtitleStreamIndex = meta.getInt(IJKM_KEY_TIMEDTEXT_STREAM, -1);\n\n        ArrayList<Bundle> streams = meta\n                .getParcelableArrayList(IJKM_KEY_STREAMS);\n        if (streams == null)\n            return meta;\n\n        int index = -1;\n        for (Bundle streamBundle : streams) {\n            index++;\n\n            if (streamBundle == null) {\n                continue;\n            }\n\n            IjkStreamMeta streamMeta = new IjkStreamMeta(index);\n            streamMeta.mMeta = streamBundle;\n            streamMeta.mType = streamMeta.getString(IJKM_KEY_TYPE);\n            streamMeta.mLanguage = streamMeta.getString(IJKM_KEY_LANGUAGE);\n            if (TextUtils.isEmpty(streamMeta.mType))\n                continue;\n\n            streamMeta.mCodecName = streamMeta.getString(IJKM_KEY_CODEC_NAME);\n            streamMeta.mCodecProfile = streamMeta\n                    .getString(IJKM_KEY_CODEC_PROFILE);\n            streamMeta.mCodecLongName = streamMeta\n                    .getString(IJKM_KEY_CODEC_LONG_NAME);\n            streamMeta.mBitrate = streamMeta.getInt(IJKM_KEY_BITRATE);\n\n            if (streamMeta.mType.equalsIgnoreCase(IJKM_VAL_TYPE__VIDEO)) {\n                streamMeta.mWidth = streamMeta.getInt(IJKM_KEY_WIDTH);\n                streamMeta.mHeight = streamMeta.getInt(IJKM_KEY_HEIGHT);\n                streamMeta.mFpsNum = streamMeta.getInt(IJKM_KEY_FPS_NUM);\n                streamMeta.mFpsDen = streamMeta.getInt(IJKM_KEY_FPS_DEN);\n                streamMeta.mTbrNum = streamMeta.getInt(IJKM_KEY_TBR_NUM);\n                streamMeta.mTbrDen = streamMeta.getInt(IJKM_KEY_TBR_DEN);\n                streamMeta.mSarNum = streamMeta.getInt(IJKM_KEY_SAR_NUM);\n                streamMeta.mSarDen = streamMeta.getInt(IJKM_KEY_SAR_DEN);\n\n                if (videoStreamIndex == index) {\n                    meta.mVideoStream = streamMeta;\n                }\n            } else if (streamMeta.mType.equalsIgnoreCase(IJKM_VAL_TYPE__AUDIO)) {\n                streamMeta.mSampleRate = streamMeta\n                        .getInt(IJKM_KEY_SAMPLE_RATE);\n                streamMeta.mChannelLayout = streamMeta\n                        .getLong(IJKM_KEY_CHANNEL_LAYOUT);\n\n                if (audioStreamIndex == index) {\n                    meta.mAudioStream = streamMeta;\n                }\n            }\n            meta.mStreams.add(streamMeta);\n        }\n\n        return meta;\n    }\n\n    public static class IjkStreamMeta {\n        public Bundle mMeta;\n\n        public final int mIndex;\n        public String mType;\n        public String mLanguage;\n\n        // common\n        public String mCodecName;\n        public String mCodecProfile;\n        public String mCodecLongName;\n        public long mBitrate;\n\n        // video\n        public int mWidth;\n        public int mHeight;\n        public int mFpsNum;\n        public int mFpsDen;\n        public int mTbrNum;\n        public int mTbrDen;\n        public int mSarNum;\n        public int mSarDen;\n\n        // audio\n        public int mSampleRate;\n        public long mChannelLayout;\n\n        public IjkStreamMeta(int index) {\n            mIndex = index;\n        }\n\n        public String getString(String key) {\n            return mMeta.getString(key);\n        }\n\n        public int getInt(String key) {\n            return getInt(key, 0);\n        }\n\n        public int getInt(String key, int defaultValue) {\n            String value = getString(key);\n            if (TextUtils.isEmpty(value))\n                return defaultValue;\n\n            try {\n                return Integer.parseInt(value);\n            } catch (NumberFormatException e) {\n                return defaultValue;\n            }\n        }\n\n        public long getLong(String key) {\n            return getLong(key, 0);\n        }\n\n        public long getLong(String key, long defaultValue) {\n            String value = getString(key);\n            if (TextUtils.isEmpty(value))\n                return defaultValue;\n\n            try {\n                return Long.parseLong(value);\n            } catch (NumberFormatException e) {\n                return defaultValue;\n            }\n        }\n\n        public String getCodecLongNameInline() {\n            if (!TextUtils.isEmpty(mCodecLongName)) {\n                return mCodecLongName;\n            } else if (!TextUtils.isEmpty(mCodecName)) {\n                return mCodecName;\n            } else {\n                return \"N/A\";\n            }\n        }\n\n        public String getCodecShortNameInline() {\n            if (!TextUtils.isEmpty(mCodecName)) {\n                return mCodecName;\n            } else {\n                return \"N/A\";\n            }\n        }\n\n        public String getResolutionInline() {\n            if (mWidth <= 0 || mHeight <= 0) {\n                return \"N/A\";\n            } else if (mSarNum <= 0 || mSarDen <= 0) {\n                return String.format(Locale.US, \"%d x %d\", mWidth, mHeight);\n            } else {\n                return String.format(Locale.US, \"%d x %d [SAR %d:%d]\", mWidth,\n                        mHeight, mSarNum, mSarDen);\n            }\n        }\n\n        public String getFpsInline() {\n            if (mFpsNum <= 0 || mFpsDen <= 0) {\n                return \"N/A\";\n            } else {\n                return String.valueOf(((float) (mFpsNum)) / mFpsDen);\n            }\n        }\n\n        public String getBitrateInline() {\n            if (mBitrate <= 0) {\n                return \"N/A\";\n            } else if (mBitrate < 1000) {\n                return String.format(Locale.US, \"%d bit/s\", mBitrate);\n            } else {\n                return String.format(Locale.US, \"%d kb/s\", mBitrate / 1000);\n            }\n        }\n\n        public String getSampleRateInline() {\n            if (mSampleRate <= 0) {\n                return \"N/A\";\n            } else {\n                return String.format(Locale.US, \"%d Hz\", mSampleRate);\n            }\n        }\n\n        public String getChannelLayoutInline() {\n            if (mChannelLayout <= 0) {\n                return \"N/A\";\n            } else {\n                if (mChannelLayout == AV_CH_LAYOUT_MONO) {\n                    return \"mono\";\n                } else if (mChannelLayout == AV_CH_LAYOUT_STEREO) {\n                    return \"stereo\";\n                } else {\n                    return String.format(Locale.US, \"%x\", mChannelLayout);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IjkMediaPlayer.java",
    "content": "/*\n * Copyright (C) 2006 Bilibili\n * Copyright (C) 2006 The Android Open Source Project\n * Copyright (C) 2013 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.res.AssetFileDescriptor;\nimport android.graphics.SurfaceTexture;\nimport android.graphics.Rect;\nimport android.media.MediaCodecInfo;\nimport android.media.MediaCodecList;\nimport android.media.RingtoneManager;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.os.ParcelFileDescriptor;\nimport android.os.PowerManager;\nimport android.provider.Settings;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\n\nimport com.android.baselib.utils.LogUtils;\n\nimport java.io.FileDescriptor;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Field;\nimport java.security.InvalidParameterException;\nimport java.util.ArrayList;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.annotations.AccessedByNative;\nimport tv.danmaku.ijk.media.player.annotations.CalledByNative;\nimport tv.danmaku.ijk.media.player.misc.IAndroidIO;\nimport tv.danmaku.ijk.media.player.misc.IMediaDataSource;\nimport tv.danmaku.ijk.media.player.misc.ITrackInfo;\nimport tv.danmaku.ijk.media.player.misc.IjkTrackInfo;\nimport tv.danmaku.ijk.media.player.pragma.DebugLog;\n\n/**\n * @author bbcallen\n *\n *         Java wrapper of ffplay.\n */\npublic final class IjkMediaPlayer extends AbstractMediaPlayer {\n    private final static String TAG = IjkMediaPlayer.class.getName();\n\n    private static final int MEDIA_NOP = 0; // interface test message\n    private static final int MEDIA_PREPARED = 1;\n    private static final int MEDIA_PLAYBACK_COMPLETE = 2;\n    private static final int MEDIA_BUFFERING_UPDATE = 3;\n    private static final int MEDIA_SEEK_COMPLETE = 4;\n    private static final int MEDIA_SET_VIDEO_SIZE = 5;\n    private static final int MEDIA_TIMED_TEXT = 99;\n    private static final int MEDIA_ERROR = 100;\n    private static final int MEDIA_INFO = 200;\n\n    protected static final int MEDIA_SET_VIDEO_SAR = 10001;\n    protected static final int MEDIA_SET_VIDEO_DAR = 10002;\n\n    //----------------------------------------\n    // options\n    public static final int IJK_LOG_UNKNOWN = 0;\n    public static final int IJK_LOG_DEFAULT = 1;\n\n    public static final int IJK_LOG_VERBOSE = 2;\n    public static final int IJK_LOG_DEBUG = 3;\n    public static final int IJK_LOG_INFO = 4;\n    public static final int IJK_LOG_WARN = 5;\n    public static final int IJK_LOG_ERROR = 6;\n    public static final int IJK_LOG_FATAL = 7;\n    public static final int IJK_LOG_SILENT = 8;\n\n    public static final int OPT_CATEGORY_FORMAT     = 1;\n    public static final int OPT_CATEGORY_CODEC      = 2;\n    public static final int OPT_CATEGORY_SWS        = 3;\n    public static final int OPT_CATEGORY_PLAYER     = 4;\n\n    public static final int SDL_FCC_YV12 = 0x32315659; // YV12\n    public static final int SDL_FCC_RV16 = 0x36315652; // RGB565\n    public static final int SDL_FCC_RV32 = 0x32335652; // RGBX8888\n    //----------------------------------------\n\n    //----------------------------------------\n    // properties\n    public static final int PROP_FLOAT_VIDEO_DECODE_FRAMES_PER_SECOND       = 10001;\n    public static final int PROP_FLOAT_VIDEO_OUTPUT_FRAMES_PER_SECOND       = 10002;\n    public static final int FFP_PROP_FLOAT_PLAYBACK_RATE                    = 10003;\n    public static final int FFP_PROP_FLOAT_DROP_FRAME_RATE                  = 10007;\n\n    public static final int FFP_PROP_INT64_SELECTED_VIDEO_STREAM            = 20001;\n    public static final int FFP_PROP_INT64_SELECTED_AUDIO_STREAM            = 20002;\n    public static final int FFP_PROP_INT64_SELECTED_TIMEDTEXT_STREAM        = 20011;\n\n    public static final int FFP_PROP_INT64_VIDEO_DECODER                    = 20003;\n    public static final int FFP_PROP_INT64_AUDIO_DECODER                    = 20004;\n    public static final int     FFP_PROPV_DECODER_UNKNOWN                   = 0;\n    public static final int     FFP_PROPV_DECODER_AVCODEC                   = 1;\n    public static final int     FFP_PROPV_DECODER_MEDIACODEC                = 2;\n    public static final int     FFP_PROPV_DECODER_VIDEOTOOLBOX              = 3;\n    public static final int FFP_PROP_INT64_VIDEO_CACHED_DURATION            = 20005;\n    public static final int FFP_PROP_INT64_AUDIO_CACHED_DURATION            = 20006;\n    public static final int FFP_PROP_INT64_VIDEO_CACHED_BYTES               = 20007;\n    public static final int FFP_PROP_INT64_AUDIO_CACHED_BYTES               = 20008;\n    public static final int FFP_PROP_INT64_VIDEO_CACHED_PACKETS             = 20009;\n    public static final int FFP_PROP_INT64_AUDIO_CACHED_PACKETS             = 20010;\n    public static final int FFP_PROP_INT64_ASYNC_STATISTIC_BUF_BACKWARDS    = 20201;\n    public static final int FFP_PROP_INT64_ASYNC_STATISTIC_BUF_FORWARDS     = 20202;\n    public static final int FFP_PROP_INT64_ASYNC_STATISTIC_BUF_CAPACITY     = 20203;\n    public static final int FFP_PROP_INT64_TRAFFIC_STATISTIC_BYTE_COUNT     = 20204;\n    public static final int FFP_PROP_INT64_CACHE_STATISTIC_PHYSICAL_POS     = 20205;\n    public static final int FFP_PROP_INT64_CACHE_STATISTIC_FILE_FORWARDS    = 20206;\n    public static final int FFP_PROP_INT64_CACHE_STATISTIC_FILE_POS         = 20207;\n    public static final int FFP_PROP_INT64_CACHE_STATISTIC_COUNT_BYTES      = 20208;\n    public static final int FFP_PROP_INT64_LOGICAL_FILE_SIZE                = 20209;\n    public static final int FFP_PROP_INT64_SHARE_CACHE_DATA                 = 20210;\n    public static final int FFP_PROP_INT64_BIT_RATE                         = 20100;\n    public static final int FFP_PROP_INT64_TCP_SPEED                        = 20200;\n    public static final int FFP_PROP_INT64_LATEST_SEEK_LOAD_DURATION        = 20300;\n    public static final int FFP_PROP_INT64_IMMEDIATE_RECONNECT              = 20211;\n    //----------------------------------------\n\n    @AccessedByNative\n    private long mNativeMediaPlayer;\n    @AccessedByNative\n    private long mNativeMediaDataSource;\n\n    @AccessedByNative\n    private long mNativeAndroidIO;\n\n    @AccessedByNative\n    private int mNativeSurfaceTexture;\n\n    @AccessedByNative\n    private int mListenerContext;\n\n    private SurfaceHolder mSurfaceHolder;\n    private EventHandler mEventHandler;\n    private PowerManager.WakeLock mWakeLock = null;\n    private boolean mScreenOnWhilePlaying;\n    private boolean mStayAwake;\n\n    private int mVideoWidth;\n    private int mVideoHeight;\n    private int mVideoSarNum;\n    private int mVideoSarDen;\n    private int mVideoDarNum;\n    private int mVideoDarDen;\n\n    private String mDataSource;\n\n    /**\n     * Default library loader\n     * Load them by yourself, if your libraries are not installed at default place.\n     */\n    private static final IjkLibLoader sLocalLibLoader = new IjkLibLoader() {\n        @Override\n        public void loadLibrary(String libName) throws UnsatisfiedLinkError, SecurityException {\n            System.loadLibrary(libName);\n        }\n    };\n\n    private static volatile boolean mIsLibLoaded = false;\n    public static void loadLibrariesOnce(IjkLibLoader libLoader) {\n        synchronized (IjkMediaPlayer.class) {\n            if (!mIsLibLoaded) {\n                if (libLoader == null)\n                    libLoader = sLocalLibLoader;\n\n                libLoader.loadLibrary(\"ijkffmpeg\");\n                libLoader.loadLibrary(\"ijksdl\");\n                libLoader.loadLibrary(\"ijkplayer\");\n                mIsLibLoaded = true;\n            }\n        }\n    }\n\n    private static volatile boolean mIsNativeInitialized = false;\n    private static void initNativeOnce() {\n        synchronized (IjkMediaPlayer.class) {\n            if (!mIsNativeInitialized) {\n                native_init();\n                mIsNativeInitialized = true;\n            }\n        }\n    }\n\n    /**\n     * Default constructor. Consider using one of the create() methods for\n     * synchronously instantiating a IjkMediaPlayer from a Uri or resource.\n     * <p>\n     * When done with the IjkMediaPlayer, you should call {@link #release()}, to\n     * free the resources. If not released, too many IjkMediaPlayer instances\n     * may result in an exception.\n     * </p>\n     */\n    public IjkMediaPlayer() {\n        this(sLocalLibLoader);\n    }\n\n    /**\n     * do not loadLibaray\n     * @param libLoader\n     *              custom library loader, can be null.\n     */\n    public IjkMediaPlayer(IjkLibLoader libLoader) {\n        initPlayer(libLoader);\n    }\n\n    private void initPlayer(IjkLibLoader libLoader) {\n        loadLibrariesOnce(libLoader);\n        initNativeOnce();\n\n        Looper looper;\n        if ((looper = Looper.myLooper()) != null) {\n            mEventHandler = new EventHandler(this, looper);\n        } else if ((looper = Looper.getMainLooper()) != null) {\n            mEventHandler = new EventHandler(this, looper);\n        } else {\n            mEventHandler = null;\n        }\n\n        /*\n         * Native setup requires a weak reference to our object. It's easier to\n         * create it here than in C++.\n         */\n        native_setup(new WeakReference<IjkMediaPlayer>(this));\n    }\n\n    private native void _setFrameAtTime(String imgCachePath, long startTime, long endTime, int num, int imgDefinition)\n            throws IllegalArgumentException, IllegalStateException;\n\n    /*\n     * Update the IjkMediaPlayer SurfaceTexture. Call after setting a new\n     * display surface.\n     */\n    private native void _setVideoSurface(Surface surface);\n\n    /**\n     * Sets the {@link SurfaceHolder} to use for displaying the video portion of\n     * the media.\n     *\n     * Either a surface holder or surface must be set if a display or video sink\n     * is needed. Not calling this method or {@link #setSurface(Surface)} when\n     * playing back a video will result in only the audio track being played. A\n     * null surface holder or surface will result in only the audio track being\n     * played.\n     *\n     * @param sh\n     *            the SurfaceHolder to use for video display\n     */\n    @Override\n    public void setDisplay(SurfaceHolder sh) {\n        mSurfaceHolder = sh;\n        Surface surface;\n        if (sh != null) {\n            surface = sh.getSurface();\n        } else {\n            surface = null;\n        }\n        _setVideoSurface(surface);\n        updateSurfaceScreenOn();\n    }\n\n    /**\n     * Sets the {@link Surface} to be used as the sink for the video portion of\n     * the media. This is similar to {@link #setDisplay(SurfaceHolder)}, but\n     * does not support {@link #setScreenOnWhilePlaying(boolean)}. Setting a\n     * Surface will un-set any Surface or SurfaceHolder that was previously set.\n     * A null surface will result in only the audio track being played.\n     *\n     * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps\n     * returned from {@link SurfaceTexture#getTimestamp()} will have an\n     * unspecified zero point. These timestamps cannot be directly compared\n     * between different media sources, different instances of the same media\n     * source, or multiple runs of the same program. The timestamp is normally\n     * monotonically increasing and is unaffected by time-of-day adjustments,\n     * but it is reset when the position is set.\n     *\n     * @param surface\n     *            The {@link Surface} to be used for the video portion of the\n     *            media.\n     */\n    @Override\n    public void setSurface(Surface surface) {\n        if (mScreenOnWhilePlaying && surface != null) {\n            DebugLog.w(TAG,\n                    \"setScreenOnWhilePlaying(true) is ineffective for Surface\");\n        }\n        mSurfaceHolder = null;\n        _setVideoSurface(surface);\n        updateSurfaceScreenOn();\n    }\n\n    /**\n     * Sets the data source as a content Uri.\n     *\n     * @param context the Context to use when resolving the Uri\n     * @param uri the Content URI of the data you want to play\n     * @throws IllegalStateException if it is called in an invalid state\n     */\n    @Override\n    public void setDataSource(Context context, Uri uri)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        setDataSource(context, uri, null);\n    }\n\n    /**\n     * Sets the data source as a content Uri.\n     *\n     * @param context the Context to use when resolving the Uri\n     * @param uri the Content URI of the data you want to play\n     * @param headers the headers to be sent together with the request for the data\n     *                Note that the cross domain redirection is allowed by default, but that can be\n     *                changed with key/value pairs through the headers parameter with\n     *                \"android-allow-cross-domain-redirect\" as the key and \"0\" or \"1\" as the value\n     *                to disallow or allow cross domain redirection.\n     * @throws IllegalStateException if it is called in an invalid state\n     */\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        final String scheme = uri.getScheme();\n        if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n            setDataSource(uri.getPath());\n            return;\n        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)\n                && Settings.AUTHORITY.equals(uri.getAuthority())) {\n            // Redirect ringtones to go directly to underlying provider\n            uri = RingtoneManager.getActualDefaultRingtoneUri(context,\n                    RingtoneManager.getDefaultType(uri));\n            if (uri == null) {\n                throw new FileNotFoundException(\"Failed to resolve default ringtone\");\n            }\n        }\n\n        AssetFileDescriptor fd = null;\n        try {\n            ContentResolver resolver = context.getContentResolver();\n            fd = resolver.openAssetFileDescriptor(uri, \"r\");\n            if (fd == null) {\n                return;\n            }\n            // Note: using getDeclaredLength so that our behavior is the same\n            // as previous versions when the content provider is returning\n            // a full file.\n            if (fd.getDeclaredLength() < 0) {\n                setDataSource(fd.getFileDescriptor());\n            } else {\n                setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getDeclaredLength());\n            }\n            return;\n        } catch (SecurityException ignored) {\n        } catch (IOException ignored) {\n        } finally {\n            if (fd != null) {\n                fd.close();\n            }\n        }\n\n        Log.d(TAG, \"Couldn't open file on client side, trying server side\");\n\n        setDataSource(uri.toString(), headers);\n    }\n\n    /**\n     * Sets the data source (file-path or http/rtsp URL) to use.\n     *\n     * @param path\n     *            the path of the file, or the http/rtsp URL of the stream you\n     *            want to play\n     * @throws IllegalStateException\n     *             if it is called in an invalid state\n     *\n     *             <p>\n     *             When <code>path</code> refers to a local file, the file may\n     *             actually be opened by a process other than the calling\n     *             application. This implies that the pathname should be an\n     *             absolute path (as any other process runs with unspecified\n     *             current working directory), and that the pathname should\n     *             reference a world-readable file.\n     */\n    @Override\n    public void setDataSource(String path)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mDataSource = path;\n        _setDataSource(path, null, null);\n    }\n\n    /**\n     * Sets the data source (file-path or http/rtsp URL) to use.\n     *\n     * @param path the path of the file, or the http/rtsp URL of the stream you want to play\n     * @param headers the headers associated with the http request for the stream you want to play\n     * @throws IllegalStateException if it is called in an invalid state\n     */\n    public void setDataSource(String path, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException\n    {\n        if (headers != null && !headers.isEmpty()) {\n            StringBuilder sb = new StringBuilder();\n            for(Map.Entry<String, String> entry: headers.entrySet()) {\n                sb.append(entry.getKey());\n                sb.append(\":\");\n                String value = entry.getValue();\n                if (!TextUtils.isEmpty(value))\n                    sb.append(entry.getValue());\n                sb.append(\"\\r\\n\");\n                setOption(OPT_CATEGORY_FORMAT, \"headers\", sb.toString());\n                setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, \"protocol_whitelist\", \"async,cache,crypto,file,http,https,ijkhttphook,ijkinject,ijklivehook,ijklongurl,ijksegment,ijktcphook,pipe,rtp,tcp,tls,udp,ijkurlhook,data\");\n            }\n        }\n        setDataSource(path);\n    }\n\n    /**\n     * Sets the data source (FileDescriptor) to use. It is the caller's responsibility\n     * to close the file descriptor. It is safe to do so as soon as this call returns.\n     *\n     * @param fd the FileDescriptor for the file you want to play\n     * @throws IllegalStateException if it is called in an invalid state\n     */\n    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)\n    @Override\n    public void setDataSource(FileDescriptor fd)\n            throws IOException, IllegalArgumentException, IllegalStateException {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) {\n            int native_fd = -1;\n            try {\n                Field f = fd.getClass().getDeclaredField(\"descriptor\"); //NoSuchFieldException\n                f.setAccessible(true);\n                native_fd = f.getInt(fd); //IllegalAccessException\n            } catch (NoSuchFieldException e) {\n                throw new RuntimeException(e);\n            } catch (IllegalAccessException e) {\n                throw new RuntimeException(e);\n            }\n            _setDataSourceFd(native_fd);\n        } else {\n            ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(fd);\n            try {\n                _setDataSourceFd(pfd.getFd());\n            } finally {\n                pfd.close();\n            }\n        }\n    }\n\n    /**\n     * Sets the data source (FileDescriptor) to use.  The FileDescriptor must be\n     * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility\n     * to close the file descriptor. It is safe to do so as soon as this call returns.\n     *\n     * @param fd the FileDescriptor for the file you want to play\n     * @param offset the offset into the file where the data to be played starts, in bytes\n     * @param length the length in bytes of the data to be played\n     * @throws IllegalStateException if it is called in an invalid state\n     */\n    private void setDataSource(FileDescriptor fd, long offset, long length)\n            throws IOException, IllegalArgumentException, IllegalStateException {\n        // FIXME: handle offset, length\n        setDataSource(fd);\n    }\n\n    public void setDataSource(IMediaDataSource mediaDataSource)\n            throws IllegalArgumentException, SecurityException, IllegalStateException {\n        _setDataSource(mediaDataSource);\n    }\n\n    public void setAndroidIOCallback(IAndroidIO androidIO)\n            throws IllegalArgumentException, SecurityException, IllegalStateException {\n        _setAndroidIOCallback(androidIO);\n    }\n\n    private native void _setDataSource(String path, String[] keys, String[] values)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    private native void _setDataSourceFd(int fd)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    private native void _setDataSource(IMediaDataSource mediaDataSource)\n            throws IllegalArgumentException, SecurityException, IllegalStateException;\n\n    private native void _setAndroidIOCallback(IAndroidIO androidIO)\n            throws IllegalArgumentException, SecurityException, IllegalStateException;\n\n    @Override\n    public String getDataSource() {\n        return mDataSource;\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        _prepareAsync();\n    }\n\n    public native void _prepareAsync() throws IllegalStateException;\n\n    @Override\n    public void start() throws IllegalStateException {\n        stayAwake(true);\n        _start();\n    }\n\n    private native void _start() throws IllegalStateException;\n\n    @Override\n    public void stop() throws IllegalStateException {\n        stayAwake(false);\n        _stop();\n    }\n\n    private native void _stop() throws IllegalStateException;\n\n    @Override\n    public void pause() throws IllegalStateException {\n        stayAwake(false);\n        _pause();\n    }\n\n    private native void _pause() throws IllegalStateException;\n\n    @SuppressLint(\"Wakelock\")\n    @Override\n    public void setWakeMode(Context context, int mode) {\n        boolean washeld = false;\n        if (mWakeLock != null) {\n            if (mWakeLock.isHeld()) {\n                washeld = true;\n                mWakeLock.release();\n            }\n            mWakeLock = null;\n        }\n\n        PowerManager pm = (PowerManager) context\n                .getSystemService(Context.POWER_SERVICE);\n        mWakeLock = pm.newWakeLock(mode | PowerManager.ON_AFTER_RELEASE,\n                IjkMediaPlayer.class.getName());\n        mWakeLock.setReferenceCounted(false);\n        if (washeld) {\n            mWakeLock.acquire();\n        }\n    }\n\n    @Override\n    public void setScreenOnWhilePlaying(boolean screenOn) {\n        if (mScreenOnWhilePlaying != screenOn) {\n            if (screenOn && mSurfaceHolder == null) {\n                DebugLog.w(TAG,\n                        \"setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder\");\n            }\n            mScreenOnWhilePlaying = screenOn;\n            updateSurfaceScreenOn();\n        }\n    }\n\n    @SuppressLint(\"Wakelock\")\n    private void stayAwake(boolean awake) {\n        if (mWakeLock != null) {\n            if (awake && !mWakeLock.isHeld()) {\n                mWakeLock.acquire();\n            } else if (!awake && mWakeLock.isHeld()) {\n                mWakeLock.release();\n            }\n        }\n        mStayAwake = awake;\n        updateSurfaceScreenOn();\n    }\n\n    private void updateSurfaceScreenOn() {\n        if (mSurfaceHolder != null) {\n            mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake);\n        }\n    }\n\n    @Override\n    public IjkTrackInfo[] getTrackInfo() {\n        Bundle bundle = getMediaMeta();\n        if (bundle == null)\n            return null;\n\n        IjkMediaMeta mediaMeta = IjkMediaMeta.parse(bundle);\n        if (mediaMeta == null || mediaMeta.mStreams == null)\n            return null;\n\n        ArrayList<IjkTrackInfo> trackInfos = new ArrayList<IjkTrackInfo>();\n        for (IjkMediaMeta.IjkStreamMeta streamMeta: mediaMeta.mStreams) {\n            IjkTrackInfo trackInfo = new IjkTrackInfo(streamMeta);\n            if (streamMeta.mType.equalsIgnoreCase(IjkMediaMeta.IJKM_VAL_TYPE__VIDEO)) {\n                trackInfo.setTrackType(ITrackInfo.MEDIA_TRACK_TYPE_VIDEO);\n            } else if (streamMeta.mType.equalsIgnoreCase(IjkMediaMeta.IJKM_VAL_TYPE__AUDIO)) {\n                trackInfo.setTrackType(ITrackInfo.MEDIA_TRACK_TYPE_AUDIO);\n            } else if (streamMeta.mType.equalsIgnoreCase(IjkMediaMeta.IJKM_VAL_TYPE__TIMEDTEXT)) {\n                trackInfo.setTrackType(ITrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT);\n            }\n            trackInfos.add(trackInfo);\n        }\n\n        return trackInfos.toArray(new IjkTrackInfo[trackInfos.size()]);\n    }\n\n    // TODO: @Override\n    public int getSelectedTrack(int trackType) {\n        switch (trackType) {\n            case ITrackInfo.MEDIA_TRACK_TYPE_VIDEO:\n                return (int)_getPropertyLong(FFP_PROP_INT64_SELECTED_VIDEO_STREAM, -1);\n            case ITrackInfo.MEDIA_TRACK_TYPE_AUDIO:\n                return (int)_getPropertyLong(FFP_PROP_INT64_SELECTED_AUDIO_STREAM, -1);\n            case ITrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT:\n                return (int)_getPropertyLong(FFP_PROP_INT64_SELECTED_TIMEDTEXT_STREAM, -1);\n            default:\n                return -1;\n        }\n    }\n\n    // experimental, should set DEFAULT_MIN_FRAMES and MAX_MIN_FRAMES to 25\n    // TODO: @Override\n    public void selectTrack(int track) {\n        _setStreamSelected(track, true);\n    }\n\n    // experimental, should set DEFAULT_MIN_FRAMES and MAX_MIN_FRAMES to 25\n    // TODO: @Override\n    public void deselectTrack(int track) {\n        _setStreamSelected(track, false);\n    }\n\n    private native void _setStreamSelected(int stream, boolean select);\n\n    @Override\n    public int getVideoWidth() {\n        return mVideoWidth;\n    }\n\n    @Override\n    public int getVideoHeight() {\n        return mVideoHeight;\n    }\n\n    @Override\n    public int getVideoSarNum() {\n        return mVideoSarNum;\n    }\n\n    @Override\n    public int getVideoSarDen() {\n        return mVideoSarDen;\n    }\n\n    @Override\n    public native boolean isPlaying();\n\n    @Override\n    public native void seekTo(long msec) throws IllegalStateException;\n\n    @Override\n    public native long getCurrentPosition();\n\n    @Override\n    public native long getDuration();\n\n    /**\n     * Releases resources associated with this IjkMediaPlayer object. It is\n     * considered good practice to call this method when you're done using the\n     * IjkMediaPlayer. In particular, whenever an Activity of an application is\n     * paused (its onPause() method is called), or stopped (its onStop() method\n     * is called), this method should be invoked to release the IjkMediaPlayer\n     * object, unless the application has a special need to keep the object\n     * around. In addition to unnecessary resources (such as memory and\n     * instances of codecs) being held, failure to call this method immediately\n     * if a IjkMediaPlayer object is no longer needed may also lead to\n     * continuous battery consumption for mobile devices, and playback failure\n     * for other applications if no multiple instances of the same codec are\n     * supported on a device. Even if multiple instances of the same codec are\n     * supported, some performance degradation may be expected when unnecessary\n     * multiple instances are used at the same time.\n     */\n    @Override\n    public void release() {\n        stayAwake(false);\n        updateSurfaceScreenOn();\n        resetListeners();\n        _release();\n    }\n\n    private native void _release();\n\n    @Override\n    public void reset() {\n        stayAwake(false);\n        _reset();\n        // make sure none of the listeners get called anymore\n        mEventHandler.removeCallbacksAndMessages(null);\n\n        mVideoWidth = 0;\n        mVideoHeight = 0;\n    }\n\n    private native void _reset();\n\n    /**\n     * Sets the player to be looping or non-looping.\n     *\n     * @param looping whether to loop or not\n     */\n    @Override\n    public void setLooping(boolean looping) {\n        int loopCount = looping ? 0 : 1;\n        setOption(OPT_CATEGORY_PLAYER, \"loop\", loopCount);\n        _setLoopCount(loopCount);\n    }\n\n    private native void _setLoopCount(int loopCount);\n\n    /**\n     * Checks whether the MediaPlayer is looping or non-looping.\n     *\n     * @return true if the MediaPlayer is currently looping, false otherwise\n     */\n    @Override\n    public boolean isLooping() {\n        int loopCount = _getLoopCount();\n        return loopCount != 1;\n    }\n\n    private native int _getLoopCount();\n\n    public void setSpeed(float speed) {\n        _setPropertyFloat(FFP_PROP_FLOAT_PLAYBACK_RATE, speed);\n    }\n\n    public float getSpeed(float speed) {\n        return _getPropertyFloat(FFP_PROP_FLOAT_PLAYBACK_RATE, .0f);\n    }\n\n    public int getVideoDecoder() {\n        return (int)_getPropertyLong(FFP_PROP_INT64_VIDEO_DECODER, FFP_PROPV_DECODER_UNKNOWN);\n    }\n\n    public float getVideoOutputFramesPerSecond() {\n        return _getPropertyFloat(PROP_FLOAT_VIDEO_OUTPUT_FRAMES_PER_SECOND, 0.0f);\n    }\n\n    public float getVideoDecodeFramesPerSecond() {\n        return _getPropertyFloat(PROP_FLOAT_VIDEO_DECODE_FRAMES_PER_SECOND, 0.0f);\n    }\n\n    public long getVideoCachedDuration() {\n        return _getPropertyLong(FFP_PROP_INT64_VIDEO_CACHED_DURATION, 0);\n    }\n\n    public long getAudioCachedDuration() {\n        return _getPropertyLong(FFP_PROP_INT64_AUDIO_CACHED_DURATION, 0);\n    }\n\n    public long getVideoCachedBytes() {\n        return _getPropertyLong(FFP_PROP_INT64_VIDEO_CACHED_BYTES, 0);\n    }\n\n    public long getAudioCachedBytes() {\n        return _getPropertyLong(FFP_PROP_INT64_AUDIO_CACHED_BYTES, 0);\n    }\n\n    public long getVideoCachedPackets() {\n        return _getPropertyLong(FFP_PROP_INT64_VIDEO_CACHED_PACKETS, 0);\n    }\n\n    public long getAudioCachedPackets() {\n        return _getPropertyLong(FFP_PROP_INT64_AUDIO_CACHED_PACKETS, 0);\n    }\n\n    public long getAsyncStatisticBufBackwards() {\n        return _getPropertyLong(FFP_PROP_INT64_ASYNC_STATISTIC_BUF_BACKWARDS, 0);\n    }\n\n    public long getAsyncStatisticBufForwards() {\n        return _getPropertyLong(FFP_PROP_INT64_ASYNC_STATISTIC_BUF_FORWARDS, 0);\n    }\n\n    public long getAsyncStatisticBufCapacity() {\n        return _getPropertyLong(FFP_PROP_INT64_ASYNC_STATISTIC_BUF_CAPACITY, 0);\n    }\n\n    public long getTrafficStatisticByteCount() {\n        return _getPropertyLong(FFP_PROP_INT64_TRAFFIC_STATISTIC_BYTE_COUNT, 0);\n    }\n\n    public long getCacheStatisticPhysicalPos() {\n        return _getPropertyLong(FFP_PROP_INT64_CACHE_STATISTIC_PHYSICAL_POS, 0);\n    }\n\n    public long getCacheStatisticFileForwards() {\n        return _getPropertyLong(FFP_PROP_INT64_CACHE_STATISTIC_FILE_FORWARDS, 0);\n    }\n\n    public long getCacheStatisticFilePos() {\n        return _getPropertyLong(FFP_PROP_INT64_CACHE_STATISTIC_FILE_POS, 0);\n    }\n\n    public long getCacheStatisticCountBytes() {\n        return _getPropertyLong(FFP_PROP_INT64_CACHE_STATISTIC_COUNT_BYTES, 0);\n    }\n\n    public long getFileSize() {\n        return _getPropertyLong(FFP_PROP_INT64_LOGICAL_FILE_SIZE, 0);\n    }\n\n    public long getBitRate() {\n        return _getPropertyLong(FFP_PROP_INT64_BIT_RATE, 0);\n    }\n\n    public long getTcpSpeed() {\n        return _getPropertyLong(FFP_PROP_INT64_TCP_SPEED, 0);\n    }\n\n    public long getSeekLoadDuration() {\n        return _getPropertyLong(FFP_PROP_INT64_LATEST_SEEK_LOAD_DURATION, 0);\n    }\n\n    private native float _getPropertyFloat(int property, float defaultValue);\n    private native void  _setPropertyFloat(int property, float value);\n    private native long  _getPropertyLong(int property, long defaultValue);\n    private native void  _setPropertyLong(int property, long value);\n\n    public float getDropFrameRate() {\n        return _getPropertyFloat(FFP_PROP_FLOAT_DROP_FRAME_RATE, .0f);\n    }\n\n    @Override\n    public native void setVolume(float leftVolume, float rightVolume);\n\n    @Override\n    public native int getAudioSessionId();\n\n    @Override\n    public MediaInfo getMediaInfo() {\n        MediaInfo mediaInfo = new MediaInfo();\n        mediaInfo.mMediaPlayerName = \"ijkplayer\";\n\n        String videoCodecInfo = _getVideoCodecInfo();\n        if (!TextUtils.isEmpty(videoCodecInfo)) {\n            String nodes[] = videoCodecInfo.split(\",\");\n            if (nodes.length >= 2) {\n                mediaInfo.mVideoDecoder = nodes[0];\n                mediaInfo.mVideoDecoderImpl = nodes[1];\n            } else if (nodes.length >= 1) {\n                mediaInfo.mVideoDecoder = nodes[0];\n                mediaInfo.mVideoDecoderImpl = \"\";\n            }\n        }\n\n        String audioCodecInfo = _getAudioCodecInfo();\n        if (!TextUtils.isEmpty(audioCodecInfo)) {\n            String nodes[] = audioCodecInfo.split(\",\");\n            if (nodes.length >= 2) {\n                mediaInfo.mAudioDecoder = nodes[0];\n                mediaInfo.mAudioDecoderImpl = nodes[1];\n            } else if (nodes.length >= 1) {\n                mediaInfo.mAudioDecoder = nodes[0];\n                mediaInfo.mAudioDecoderImpl = \"\";\n            }\n        }\n\n        try {\n            mediaInfo.mMeta = IjkMediaMeta.parse(_getMediaMeta());\n        } catch (Throwable e) {\n            e.printStackTrace();\n        }\n        return mediaInfo;\n    }\n\n    @Override\n    public void setLogEnabled(boolean enable) {\n        // do nothing\n    }\n\n    @Override\n    public boolean isPlayable() {\n        return true;\n    }\n\n    private native String _getVideoCodecInfo();\n    private native String _getAudioCodecInfo();\n\n    public void setOption(int category, String name, String value)\n    {\n        _setOption(category, name, value);\n    }\n\n    public void setOption(int category, String name, long value)\n    {\n        _setOption(category, name, value);\n    }\n\n    private native void _setOption(int category, String name, String value);\n    private native void _setOption(int category, String name, long value);\n\n    public Bundle getMediaMeta() {\n        return _getMediaMeta();\n    }\n    private native Bundle _getMediaMeta();\n\n    public static String getColorFormatName(int mediaCodecColorFormat) {\n        return _getColorFormatName(mediaCodecColorFormat);\n    }\n\n    private static native String _getColorFormatName(int mediaCodecColorFormat);\n\n    @Override\n    public void setAudioStreamType(int streamtype) {\n        // do nothing\n    }\n\n    @Override\n    public void setKeepInBackground(boolean keepInBackground) {\n        // do nothing\n    }\n\n    private static native void native_init();\n\n    private native void native_setup(Object IjkMediaPlayer_this);\n\n    private native void native_finalize();\n\n    private native void native_message_loop(Object IjkMediaPlayer_this);\n\n    protected void finalize() throws Throwable {\n        super.finalize();\n        native_finalize();\n    }\n\n    public void httphookReconnect() {\n        _setPropertyLong(FFP_PROP_INT64_IMMEDIATE_RECONNECT, 1);\n    }\n\n    public void setCacheShare(int share) {\n        _setPropertyLong(FFP_PROP_INT64_SHARE_CACHE_DATA, (long)share);\n    }\n\n    private static class EventHandler extends Handler {\n        private final WeakReference<IjkMediaPlayer> mWeakPlayer;\n\n        public EventHandler(IjkMediaPlayer mp, Looper looper) {\n            super(looper);\n            mWeakPlayer = new WeakReference<IjkMediaPlayer>(mp);\n        }\n\n        @Override\n        public void handleMessage(Message msg) {\n            IjkMediaPlayer player = mWeakPlayer.get();\n            if (player == null || player.mNativeMediaPlayer == 0) {\n                DebugLog.w(TAG,\n                        \"IjkMediaPlayer went away with unhandled events\");\n                return;\n            }\n\n            switch (msg.what) {\n            case MEDIA_PREPARED:\n                player.notifyOnPrepared();\n                return;\n\n            case MEDIA_PLAYBACK_COMPLETE:\n                player.stayAwake(false);\n                player.notifyOnCompletion();\n                return;\n\n            case MEDIA_BUFFERING_UPDATE:\n                long bufferPosition = msg.arg1;\n                if (bufferPosition < 0) {\n                    bufferPosition = 0;\n                }\n\n                long percent = 0;\n                long duration = player.getDuration();\n                if (duration > 0) {\n                    percent = bufferPosition * 100 / duration;\n                }\n                if (percent >= 100) {\n                    percent = 100;\n                }\n\n                // DebugLog.efmt(TAG, \"Buffer (%d%%) %d/%d\",  percent, bufferPosition, duration);\n                player.notifyOnBufferingUpdate((int)percent);\n                return;\n\n            case MEDIA_SEEK_COMPLETE:\n                player.notifyOnSeekComplete();\n                return;\n\n            case MEDIA_SET_VIDEO_SIZE:\n                player.mVideoWidth = msg.arg1;\n                player.mVideoHeight = msg.arg2;\n                player.notifyOnVideoSizeChanged(player.mVideoWidth, player.mVideoHeight,\n                        player.mVideoSarNum, player.mVideoSarDen);\n                return;\n\n            case MEDIA_ERROR:\n                DebugLog.e(TAG, \"Error (\" + msg.arg1 + \",\" + msg.arg2 + \")\");\n                if (!player.notifyOnError(msg.arg1, msg.arg2)) {\n                    player.notifyOnCompletion();\n                }\n                player.stayAwake(false);\n                return;\n\n            case MEDIA_INFO:\n                switch (msg.arg1) {\n                    case MEDIA_INFO_VIDEO_RENDERING_START:\n                        DebugLog.i(TAG, \"Info: MEDIA_INFO_VIDEO_RENDERING_START\\n\");\n                        break;\n                }\n                player.notifyOnInfo(msg.arg1, msg.arg2);\n                // No real default action so far.\n                return;\n            case MEDIA_TIMED_TEXT:\n                if (msg.obj == null) {\n                    player.notifyOnTimedText(null);\n                } else {\n                    IjkTimedText text = new IjkTimedText(new Rect(0, 0, 1, 1), (String)msg.obj);\n                    player.notifyOnTimedText(text);\n                }\n                return;\n            case MEDIA_NOP: // interface test message - ignore\n                break;\n\n            case MEDIA_SET_VIDEO_SAR:\n                player.mVideoSarNum = msg.arg1;\n                player.mVideoSarDen = msg.arg2;\n                player.notifyOnVideoSizeChanged(player.mVideoWidth, player.mVideoHeight,\n                        player.mVideoSarNum, player.mVideoSarDen);\n                break;\n            case MEDIA_SET_VIDEO_DAR:\n                player.mVideoDarNum = msg.arg1;\n                player.mVideoDarDen = msg.arg2;\n                player.notifyOnVideoDarSizeChanged(player.mVideoWidth, player.mVideoHeight,\n                        player.mVideoSarNum, player.mVideoSarDen, player.mVideoDarNum, player.mVideoDarDen);\n                break;\n            default:\n                DebugLog.e(TAG, \"Unknown message type \" + msg.what);\n            }\n        }\n    }\n\n    /*\n     * Called from native code when an interesting event happens. This method\n     * just uses the EventHandler system to post the event back to the main app\n     * thread. We use a weak reference to the original IjkMediaPlayer object so\n     * that the native code is safe from the object disappearing from underneath\n     * it. (This is the cookie passed to native_setup().)\n     */\n    @CalledByNative\n    private static void postEventFromNative(Object weakThiz, int what,\n            int arg1, int arg2, Object obj) {\n        if (weakThiz == null)\n            return;\n\n        @SuppressWarnings(\"rawtypes\")\n        IjkMediaPlayer mp = (IjkMediaPlayer) ((WeakReference) weakThiz).get();\n        if (mp == null) {\n            return;\n        }\n\n        if (what == MEDIA_INFO && arg1 == MEDIA_INFO_STARTED_AS_NEXT) {\n            // this acquires the wakelock if needed, and sets the client side\n            // state\n            mp.start();\n        }\n        if (mp.mEventHandler != null) {\n            Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);\n            mp.mEventHandler.sendMessage(m);\n        }\n    }\n\n    /*\n     * ControlMessage\n     */\n\n    private OnControlMessageListener mOnControlMessageListener;\n    public void setOnControlMessageListener(OnControlMessageListener listener) {\n        mOnControlMessageListener = listener;\n    }\n\n    public interface OnControlMessageListener {\n        String onControlResolveSegmentUrl(int segment);\n    }\n\n    /*\n     * NativeInvoke\n     */\n\n    private OnNativeInvokeListener mOnNativeInvokeListener;\n    public void setOnNativeInvokeListener(OnNativeInvokeListener listener) {\n        mOnNativeInvokeListener = listener;\n    }\n\n    public interface OnNativeInvokeListener {\n\n        int CTRL_WILL_TCP_OPEN = 0x20001;               // NO ARGS\n        int CTRL_DID_TCP_OPEN = 0x20002;                // ARG_ERROR, ARG_FAMILIY, ARG_IP, ARG_PORT, ARG_FD\n\n        int CTRL_WILL_HTTP_OPEN = 0x20003;              // ARG_URL, ARG_SEGMENT_INDEX, ARG_RETRY_COUNTER\n        int CTRL_WILL_LIVE_OPEN = 0x20005;              // ARG_URL, ARG_RETRY_COUNTER\n        int CTRL_WILL_CONCAT_RESOLVE_SEGMENT = 0x20007; // ARG_URL, ARG_SEGMENT_INDEX, ARG_RETRY_COUNTER\n\n        int EVENT_WILL_HTTP_OPEN = 0x1;                 // ARG_URL\n        int EVENT_DID_HTTP_OPEN = 0x2;                  // ARG_URL, ARG_ERROR, ARG_HTTP_CODE\n        int EVENT_WILL_HTTP_SEEK = 0x3;                 // ARG_URL, ARG_OFFSET\n        int EVENT_DID_HTTP_SEEK = 0x4;                  // ARG_URL, ARG_OFFSET, ARG_ERROR, ARG_HTTP_CODE, ARG_FILE_SIZE\n\n        String ARG_URL = \"url\";\n        String ARG_SEGMENT_INDEX = \"segment_index\";\n        String ARG_RETRY_COUNTER = \"retry_counter\";\n\n        String ARG_ERROR = \"error\";\n        String ARG_FAMILIY = \"family\";\n        String ARG_IP = \"ip\";\n        String ARG_PORT = \"port\";\n        String ARG_FD = \"fd\";\n\n        String ARG_OFFSET = \"offset\";\n        String ARG_HTTP_CODE = \"http_code\";\n        String ARG_FILE_SIZE = \"file_size\";\n\n        /*\n         * @return true if invoke is handled\n         * @throws Exception on any error\n         */\n        boolean onNativeInvoke(int what, Bundle args);\n    }\n\n    @CalledByNative\n    private static boolean onNativeInvoke(Object weakThiz, int what, Bundle args) {\n        DebugLog.ifmt(TAG, \"onNativeInvoke %d\", what);\n        if (weakThiz == null || !(weakThiz instanceof WeakReference<?>))\n            throw new IllegalStateException(\"<null weakThiz>.onNativeInvoke()\");\n\n        @SuppressWarnings(\"unchecked\")\n        WeakReference<IjkMediaPlayer> weakPlayer = (WeakReference<IjkMediaPlayer>) weakThiz;\n        IjkMediaPlayer player = weakPlayer.get();\n        if (player == null)\n            throw new IllegalStateException(\"<null weakPlayer>.onNativeInvoke()\");\n\n        OnNativeInvokeListener listener = player.mOnNativeInvokeListener;\n        if (listener != null && listener.onNativeInvoke(what, args))\n            return true;\n\n        switch (what) {\n            case OnNativeInvokeListener.CTRL_WILL_CONCAT_RESOLVE_SEGMENT: {\n                OnControlMessageListener onControlMessageListener = player.mOnControlMessageListener;\n                if (onControlMessageListener == null)\n                    return false;\n\n                int segmentIndex = args.getInt(OnNativeInvokeListener.ARG_SEGMENT_INDEX, -1);\n                if (segmentIndex < 0)\n                    throw new InvalidParameterException(\"onNativeInvoke(invalid segment index)\");\n\n                String newUrl = onControlMessageListener.onControlResolveSegmentUrl(segmentIndex);\n                if (newUrl == null)\n                    throw new RuntimeException(new IOException(\"onNativeInvoke() = <NULL newUrl>\"));\n\n                args.putString(OnNativeInvokeListener.ARG_URL, newUrl);\n                return true;\n            }\n            default:\n                return false;\n        }\n    }\n\n    /*\n     * MediaCodec select\n     */\n\n    public interface OnMediaCodecSelectListener {\n        String onMediaCodecSelect(IMediaPlayer mp, String mimeType, int profile, int level);\n    }\n    private OnMediaCodecSelectListener mOnMediaCodecSelectListener;\n    public void setOnMediaCodecSelectListener(OnMediaCodecSelectListener listener) {\n        mOnMediaCodecSelectListener = listener;\n    }\n\n    public void resetListeners() {\n        super.resetListeners();\n        mOnMediaCodecSelectListener = null;\n    }\n\n    @CalledByNative\n    private static String onSelectCodec(Object weakThiz, String mimeType, int profile, int level) {\n        if (weakThiz == null || !(weakThiz instanceof WeakReference<?>))\n            return null;\n\n        @SuppressWarnings(\"unchecked\")\n        WeakReference<IjkMediaPlayer> weakPlayer = (WeakReference<IjkMediaPlayer>) weakThiz;\n        IjkMediaPlayer player = weakPlayer.get();\n        if (player == null)\n            return null;\n\n        OnMediaCodecSelectListener listener = player.mOnMediaCodecSelectListener;\n        if (listener == null)\n            listener = DefaultMediaCodecSelector.sInstance;\n\n        return listener.onMediaCodecSelect(player, mimeType, profile, level);\n    }\n\n    public static class DefaultMediaCodecSelector implements OnMediaCodecSelectListener {\n        public static final DefaultMediaCodecSelector sInstance = new DefaultMediaCodecSelector();\n\n        @SuppressWarnings(\"deprecation\")\n        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n        public String onMediaCodecSelect(IMediaPlayer mp, String mimeType, int profile, int level) {\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)\n                return null;\n\n            if (TextUtils.isEmpty(mimeType))\n                return null;\n\n            Log.i(TAG, String.format(Locale.US, \"onSelectCodec: mime=%s, profile=%d, level=%d\", mimeType, profile, level));\n            ArrayList<IjkMediaCodecInfo> candidateCodecList = new ArrayList<IjkMediaCodecInfo>();\n            int numCodecs = MediaCodecList.getCodecCount();\n            for (int i = 0; i < numCodecs; i++) {\n                MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);\n                Log.d(TAG, String.format(Locale.US, \"  found codec: %s\", codecInfo.getName()));\n                if (codecInfo.isEncoder())\n                    continue;\n\n                String[] types = codecInfo.getSupportedTypes();\n                if (types == null)\n                    continue;\n\n                for(String type: types) {\n                    if (TextUtils.isEmpty(type))\n                        continue;\n\n                    Log.d(TAG, String.format(Locale.US, \"    mime: %s\", type));\n                    if (!type.equalsIgnoreCase(mimeType))\n                        continue;\n\n                    IjkMediaCodecInfo candidate = IjkMediaCodecInfo.setupCandidate(codecInfo, mimeType);\n                    if (candidate == null)\n                        continue;\n\n                    candidateCodecList.add(candidate);\n                    Log.i(TAG, String.format(Locale.US, \"candidate codec: %s rank=%d\", codecInfo.getName(), candidate.mRank));\n                    candidate.dumpProfileLevels(mimeType);\n                }\n            }\n\n            if (candidateCodecList.isEmpty()) {\n                return null;\n            }\n\n            IjkMediaCodecInfo bestCodec = candidateCodecList.get(0);\n\n            for (IjkMediaCodecInfo codec : candidateCodecList) {\n                if (codec.mRank > bestCodec.mRank) {\n                    bestCodec = codec;\n                }\n            }\n\n            if (bestCodec.mRank < IjkMediaCodecInfo.RANK_LAST_CHANCE) {\n                Log.w(TAG, String.format(Locale.US, \"unaccetable codec: %s\", bestCodec.mCodecInfo.getName()));\n                return null;\n            }\n\n            Log.i(TAG, String.format(Locale.US, \"selected codec: %s rank=%d\", bestCodec.mCodecInfo.getName(), bestCodec.mRank));\n            return bestCodec.mCodecInfo.getName();\n        }\n    }\n\n    public static native void native_profileBegin(String libName);\n    public static native void native_profileEnd();\n    public static native void native_setLogLevel(int level);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/IjkTimedText.java",
    "content": "/*\n * Copyright (C) 2016 Zheng Yuan <zhengyuan10503@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.graphics.Rect;\nimport java.lang.String;\n\npublic final class IjkTimedText {\n\n    private Rect mTextBounds = null;\n    private String mTextChars = null;\n\n    public IjkTimedText(Rect bounds, String text) {\n        mTextBounds = bounds;\n        mTextChars = text;\n    }\n\n    public Rect getBounds() {\n        return mTextBounds;\n    }\n\n    public String getText() {\n        return mTextChars;\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/MediaInfo.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\npublic class MediaInfo {\n    public String mMediaPlayerName;\n\n    public String mVideoDecoder;\n    public String mVideoDecoderImpl;\n\n    public String mAudioDecoder;\n    public String mAudioDecoderImpl;\n\n    public IjkMediaMeta mMeta;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/MediaPlayerProxy.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.misc.IMediaDataSource;\nimport tv.danmaku.ijk.media.player.misc.ITrackInfo;\n\npublic class MediaPlayerProxy implements IMediaPlayer {\n    protected final IMediaPlayer mBackEndMediaPlayer;\n\n    public MediaPlayerProxy(IMediaPlayer backEndMediaPlayer) {\n        mBackEndMediaPlayer = backEndMediaPlayer;\n    }\n\n    public IMediaPlayer getInternalMediaPlayer() {\n        return mBackEndMediaPlayer;\n    }\n\n    @Override\n    public void setDisplay(SurfaceHolder sh) {\n        mBackEndMediaPlayer.setDisplay(sh);\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    @Override\n    public void setSurface(Surface surface) {\n        mBackEndMediaPlayer.setSurface(surface);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mBackEndMediaPlayer.setDataSource(context, uri);\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mBackEndMediaPlayer.setDataSource(context, uri, headers);\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd)\n            throws IOException, IllegalArgumentException, IllegalStateException {\n        mBackEndMediaPlayer.setDataSource(fd);\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mBackEndMediaPlayer.setDataSource(path);\n    }\n\n    @Override\n    public void setDataSource(IMediaDataSource mediaDataSource)  {\n        mBackEndMediaPlayer.setDataSource(mediaDataSource);\n    }\n\n    @Override\n    public String getDataSource() {\n        return mBackEndMediaPlayer.getDataSource();\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        mBackEndMediaPlayer.prepareAsync();\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mBackEndMediaPlayer.start();\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mBackEndMediaPlayer.stop();\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mBackEndMediaPlayer.pause();\n    }\n\n    @Override\n    public void setScreenOnWhilePlaying(boolean screenOn) {\n        mBackEndMediaPlayer.setScreenOnWhilePlaying(screenOn);\n    }\n\n    @Override\n    public int getVideoWidth() {\n        return mBackEndMediaPlayer.getVideoWidth();\n    }\n\n    @Override\n    public int getVideoHeight() {\n        return mBackEndMediaPlayer.getVideoHeight();\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mBackEndMediaPlayer.isPlaying();\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        mBackEndMediaPlayer.seekTo(msec);\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return mBackEndMediaPlayer.getCurrentPosition();\n    }\n\n    @Override\n    public long getDuration() {\n        return mBackEndMediaPlayer.getDuration();\n    }\n\n    @Override\n    public void release() {\n        mBackEndMediaPlayer.release();\n    }\n\n    @Override\n    public void reset() {\n        mBackEndMediaPlayer.reset();\n    }\n\n    @Override\n    public void setVolume(float leftVolume, float rightVolume) {\n        mBackEndMediaPlayer.setVolume(leftVolume, rightVolume);\n    }\n\n    @Override\n    public int getAudioSessionId() {\n        return mBackEndMediaPlayer.getAudioSessionId();\n    }\n\n    @Override\n    public MediaInfo getMediaInfo() {\n        return mBackEndMediaPlayer.getMediaInfo();\n    }\n\n    @Override\n    public void setLogEnabled(boolean enable) {\n\n    }\n\n    @Override\n    public boolean isPlayable() {\n        return false;\n    }\n\n    @Override\n    public void setOnPreparedListener(OnPreparedListener listener) {\n        if (listener != null) {\n            final OnPreparedListener finalListener = listener;\n            mBackEndMediaPlayer.setOnPreparedListener(new OnPreparedListener() {\n                @Override\n                public void onPrepared(IMediaPlayer mp) {\n                    finalListener.onPrepared(MediaPlayerProxy.this);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnPreparedListener(null);\n        }\n    }\n\n    @Override\n    public void setOnCompletionListener(OnCompletionListener listener) {\n        if (listener != null) {\n            final OnCompletionListener finalListener = listener;\n            mBackEndMediaPlayer.setOnCompletionListener(new OnCompletionListener() {\n                @Override\n                public void onCompletion(IMediaPlayer mp) {\n                    finalListener.onCompletion(MediaPlayerProxy.this);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnCompletionListener(null);\n        }\n    }\n\n    @Override\n    public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) {\n        if (listener != null) {\n            final OnBufferingUpdateListener finalListener = listener;\n            mBackEndMediaPlayer.setOnBufferingUpdateListener(new OnBufferingUpdateListener() {\n                @Override\n                public void onBufferingUpdate(IMediaPlayer mp, int percent) {\n                    finalListener.onBufferingUpdate(MediaPlayerProxy.this, percent);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnBufferingUpdateListener(null);\n        }\n    }\n\n    @Override\n    public void setOnSeekCompleteListener(OnSeekCompleteListener listener) {\n        if (listener != null) {\n            final OnSeekCompleteListener finalListener = listener;\n            mBackEndMediaPlayer.setOnSeekCompleteListener(new OnSeekCompleteListener() {\n                @Override\n                public void onSeekComplete(IMediaPlayer mp) {\n                    finalListener.onSeekComplete(MediaPlayerProxy.this);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnSeekCompleteListener(null);\n        }\n    }\n\n    @Override\n    public void setOnVideoSizeChangedListener(OnVideoSizeChangedListener listener) {\n        if (listener != null) {\n            final OnVideoSizeChangedListener finalListener = listener;\n            mBackEndMediaPlayer.setOnVideoSizeChangedListener(new OnVideoSizeChangedListener() {\n                @Override\n                public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sar_num, int sar_den) {\n                    finalListener.onVideoSizeChanged(MediaPlayerProxy.this, width, height, sar_num, sar_den);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnVideoSizeChangedListener(null);\n        }\n    }\n\n    @Override\n    public void setOnErrorListener(OnErrorListener listener) {\n        if (listener != null) {\n            final OnErrorListener finalListener = listener;\n            mBackEndMediaPlayer.setOnErrorListener(new OnErrorListener() {\n                @Override\n                public boolean onError(IMediaPlayer mp, int what, int extra) {\n                    return finalListener.onError(MediaPlayerProxy.this, what, extra);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnErrorListener(null);\n        }\n    }\n\n    @Override\n    public void setOnInfoListener(OnInfoListener listener) {\n        if (listener != null) {\n            final OnInfoListener finalListener = listener;\n            mBackEndMediaPlayer.setOnInfoListener(new OnInfoListener() {\n                @Override\n                public boolean onInfo(IMediaPlayer mp, int what, int extra) {\n                    return finalListener.onInfo(MediaPlayerProxy.this, what, extra);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnInfoListener(null);\n        }\n    }\n\n    @Override\n    public void setOnTimedTextListener(OnTimedTextListener listener) {\n        if (listener != null) {\n            final OnTimedTextListener finalListener = listener;\n            mBackEndMediaPlayer.setOnTimedTextListener(new OnTimedTextListener() {\n                @Override\n                public void onTimedText(IMediaPlayer mp, IjkTimedText text) {\n                    finalListener.onTimedText(MediaPlayerProxy.this, text);\n                }\n            });\n        } else {\n            mBackEndMediaPlayer.setOnTimedTextListener(null);\n        }\n    }\n\n    @Override\n    public void setAudioStreamType(int streamtype) {\n        mBackEndMediaPlayer.setAudioStreamType(streamtype);\n    }\n\n    @Override\n    public void setKeepInBackground(boolean keepInBackground) {\n        mBackEndMediaPlayer.setKeepInBackground(keepInBackground);\n    }\n\n    @Override\n    public int getVideoSarNum() {\n        return mBackEndMediaPlayer.getVideoSarNum();\n    }\n\n    @Override\n    public int getVideoSarDen() {\n        return mBackEndMediaPlayer.getVideoSarDen();\n    }\n\n    @Override\n    public void setWakeMode(Context context, int mode) {\n        mBackEndMediaPlayer.setWakeMode(context, mode);\n    }\n\n    @Override\n    public ITrackInfo[] getTrackInfo() {\n        return mBackEndMediaPlayer.getTrackInfo();\n    }\n\n    @Override\n    public void setLooping(boolean looping) {\n        mBackEndMediaPlayer.setLooping(looping);\n    }\n\n    @Override\n    public boolean isLooping() {\n        return mBackEndMediaPlayer.isLooping();\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/TextureMediaPlayer.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player;\n\nimport android.annotation.TargetApi;\nimport android.graphics.SurfaceTexture;\nimport android.os.Build;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\n\n@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\npublic class TextureMediaPlayer extends MediaPlayerProxy implements IMediaPlayer, ISurfaceTextureHolder {\n    private SurfaceTexture mSurfaceTexture;\n    private ISurfaceTextureHost mSurfaceTextureHost;\n\n    public TextureMediaPlayer(IMediaPlayer backEndMediaPlayer) {\n        super(backEndMediaPlayer);\n    }\n\n    public void releaseSurfaceTexture() {\n        if (mSurfaceTexture != null) {\n            if (mSurfaceTextureHost != null) {\n                mSurfaceTextureHost.releaseSurfaceTexture(mSurfaceTexture);\n            } else {\n                mSurfaceTexture.release();\n            }\n            mSurfaceTexture = null;\n        }\n    }\n\n    //--------------------\n    // IMediaPlayer\n    //--------------------\n    @Override\n    public void reset() {\n        super.reset();\n        releaseSurfaceTexture();\n    }\n\n    @Override\n    public void release() {\n        super.release();\n        releaseSurfaceTexture();\n    }\n\n    @Override\n    public void setDisplay(SurfaceHolder sh) {\n        if (mSurfaceTexture == null)\n            super.setDisplay(sh);\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n        if (mSurfaceTexture == null)\n            super.setSurface(surface);\n    }\n\n    //--------------------\n    // ISurfaceTextureHolder\n    //--------------------\n\n    @Override\n    public void setSurfaceTexture(SurfaceTexture surfaceTexture) {\n        if (mSurfaceTexture == surfaceTexture)\n            return;\n\n        releaseSurfaceTexture();\n        mSurfaceTexture = surfaceTexture;\n        if (surfaceTexture == null) {\n            super.setSurface(null);\n        } else {\n            super.setSurface(new Surface(surfaceTexture));\n        }\n    }\n\n    @Override\n    public SurfaceTexture getSurfaceTexture() {\n        return mSurfaceTexture;\n    }\n\n    @Override\n    public void setSurfaceTextureHost(ISurfaceTextureHost surfaceTextureHost) {\n        mSurfaceTextureHost = surfaceTextureHost;\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/annotations/AccessedByNative.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.annotations;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * is used by the JNI generator to create the necessary JNI\n * bindings and expose this method to native code.\n */\n@Target(ElementType.FIELD)\n@Retention(RetentionPolicy.CLASS)\npublic @interface AccessedByNative {\n}"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/annotations/CalledByNative.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.annotations;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * is used by the JNI generator to create the necessary JNI\n * bindings and expose this method to native code.\n */\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.CLASS)\npublic @interface CalledByNative {\n    /*\n     * If present, tells which inner class the method belongs to.\n     */\n    String value() default \"\";\n}"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/exceptions/IjkMediaException.java",
    "content": "/*\n * Copyright (C) 2013-2014 Bilibili\n * Copyright (C) 2013-2014 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.exceptions;\n\npublic class IjkMediaException extends Exception {\n    private static final long serialVersionUID = 7234796519009099506L;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/ffmpeg/FFmpegApi.java",
    "content": "package tv.danmaku.ijk.media.player.ffmpeg;\n\npublic class FFmpegApi {\n    public static native String av_base64_encode(byte in[]);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/AndroidMediaFormat.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaFormat;\nimport android.os.Build;\n\npublic class AndroidMediaFormat implements IMediaFormat {\n    private final MediaFormat mMediaFormat;\n\n    public AndroidMediaFormat(MediaFormat mediaFormat) {\n        mMediaFormat = mediaFormat;\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public int getInteger(String name) {\n        if (mMediaFormat == null)\n            return 0;\n\n        return mMediaFormat.getInteger(name);\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public String getString(String name) {\n        if (mMediaFormat == null)\n            return null;\n\n        return mMediaFormat.getString(name);\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public String toString() {\n        StringBuilder out = new StringBuilder(128);\n        out.append(getClass().getName());\n        out.append('{');\n        if (mMediaFormat != null) {\n            out.append(mMediaFormat.toString());\n        } else {\n            out.append(\"null\");\n        }\n        out.append('}');\n        return out.toString();\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/AndroidTrackInfo.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaFormat;\nimport android.media.MediaPlayer;\nimport android.os.Build;\n\npublic class AndroidTrackInfo implements ITrackInfo {\n    private final MediaPlayer.TrackInfo mTrackInfo;\n\n    public static AndroidTrackInfo[] fromMediaPlayer(MediaPlayer mp) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)\n            return fromTrackInfo(mp.getTrackInfo());\n\n        return null;\n    }\n\n    private static AndroidTrackInfo[] fromTrackInfo(MediaPlayer.TrackInfo[] trackInfos) {\n        if (trackInfos == null)\n            return null;\n\n        AndroidTrackInfo androidTrackInfo[] = new AndroidTrackInfo[trackInfos.length];\n        for (int i = 0; i < trackInfos.length; ++i) {\n            androidTrackInfo[i] = new AndroidTrackInfo(trackInfos[i]);\n        }\n\n        return androidTrackInfo;\n    }\n\n    private AndroidTrackInfo(MediaPlayer.TrackInfo trackInfo) {\n        mTrackInfo = trackInfo;\n    }\n\n    @TargetApi(Build.VERSION_CODES.KITKAT)\n    @Override\n    public IMediaFormat getFormat() {\n        if (mTrackInfo == null)\n            return null;\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)\n            return null;\n\n        MediaFormat mediaFormat = mTrackInfo.getFormat();\n        if (mediaFormat == null)\n            return null;\n\n        return new AndroidMediaFormat(mediaFormat);\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public String getLanguage() {\n        if (mTrackInfo == null)\n            return \"und\";\n\n        return mTrackInfo.getLanguage();\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public int getTrackType() {\n        if (mTrackInfo == null)\n            return MEDIA_TRACK_TYPE_UNKNOWN;\n\n        return mTrackInfo.getTrackType();\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public String toString() {\n        StringBuilder out = new StringBuilder(128);\n        out.append(getClass().getSimpleName());\n        out.append('{');\n        if (mTrackInfo != null) {\n            out.append(mTrackInfo.toString());\n        } else {\n            out.append(\"null\");\n        }\n        out.append('}');\n        return out.toString();\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public String getInfoInline() {\n        if (mTrackInfo != null) {\n            return mTrackInfo.toString();\n        } else {\n            return \"null\";\n        }\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/IAndroidIO.java",
    "content": "/*\n * Copyright (C) 2016 Bilibili\n * Copyright (C) 2016 Raymond Zheng <raymondzheng1412@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport java.io.IOException;\n\n@SuppressWarnings(\"RedundantThrows\")\npublic interface IAndroidIO {\n    int  open(String url) throws IOException;\n    int  read(byte[] buffer, int size) throws IOException;\n    long seek(long offset, int whence) throws IOException;\n    int  close() throws IOException;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/IMediaDataSource.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport java.io.IOException;\n\n@SuppressWarnings(\"RedundantThrows\")\npublic interface IMediaDataSource {\n    int\t readAt(long position, byte[] buffer, int offset, int size) throws IOException;\n\n    long getSize() throws IOException;\n\n    void close() throws IOException;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/IMediaFormat.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\npublic interface IMediaFormat {\n    // Common keys\n    String KEY_MIME = \"mime\";\n\n    // Video Keys\n    String KEY_WIDTH = \"width\";\n    String KEY_HEIGHT = \"height\";\n\n    String getString(String name);\n\n    int getInteger(String name);\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/ITrackInfo.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\npublic interface ITrackInfo {\n    int MEDIA_TRACK_TYPE_AUDIO = 2;\n    int MEDIA_TRACK_TYPE_METADATA = 5;\n    int MEDIA_TRACK_TYPE_SUBTITLE = 4;\n    int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;\n    int MEDIA_TRACK_TYPE_UNKNOWN = 0;\n    int MEDIA_TRACK_TYPE_VIDEO = 1;\n\n    IMediaFormat getFormat();\n\n    String getLanguage();\n\n    int getTrackType();\n\n    String getInfoInline();\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/IjkMediaFormat.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.IjkMediaMeta;\n\npublic class IjkMediaFormat implements IMediaFormat {\n    // Common\n    public static final String KEY_IJK_CODEC_LONG_NAME_UI = \"ijk-codec-long-name-ui\";\n    public static final String KEY_IJK_CODEC_NAME_UI = \"ijk-codec-name-ui\";\n    public static final String KEY_IJK_BIT_RATE_UI = \"ijk-bit-rate-ui\";\n\n    // Video\n    public static final String KEY_IJK_CODEC_PROFILE_LEVEL_UI = \"ijk-profile-level-ui\";\n    public static final String KEY_IJK_CODEC_PIXEL_FORMAT_UI = \"ijk-pixel-format-ui\";\n    public static final String KEY_IJK_RESOLUTION_UI = \"ijk-resolution-ui\";\n    public static final String KEY_IJK_FRAME_RATE_UI = \"ijk-frame-rate-ui\";\n\n    // Audio\n    public static final String KEY_IJK_SAMPLE_RATE_UI = \"ijk-sample-rate-ui\";\n    public static final String KEY_IJK_CHANNEL_UI = \"ijk-channel-ui\";\n\n    // Codec\n    public static final String CODEC_NAME_H264 = \"h264\";\n\n    public final IjkMediaMeta.IjkStreamMeta mMediaFormat;\n\n    public IjkMediaFormat(IjkMediaMeta.IjkStreamMeta streamMeta) {\n        mMediaFormat = streamMeta;\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n    @Override\n    public int getInteger(String name) {\n        if (mMediaFormat == null)\n            return 0;\n\n        return mMediaFormat.getInt(name);\n    }\n\n    @Override\n    public String getString(String name) {\n        if (mMediaFormat == null)\n            return null;\n\n        if (sFormatterMap.containsKey(name)) {\n            Formatter formatter = sFormatterMap.get(name);\n            return formatter.format(this);\n        }\n\n        return mMediaFormat.getString(name);\n    }\n\n    //-------------------------\n    // Formatter\n    //-------------------------\n\n    private static abstract class Formatter {\n        public String format(IjkMediaFormat mediaFormat) {\n            String value = doFormat(mediaFormat);\n            if (TextUtils.isEmpty(value))\n                return getDefaultString();\n            return value;\n        }\n\n        protected abstract String doFormat(IjkMediaFormat mediaFormat);\n\n        @SuppressWarnings(\"SameReturnValue\")\n        protected String getDefaultString() {\n            return \"N/A\";\n        }\n    }\n\n    private static final Map<String, Formatter> sFormatterMap = new HashMap<String, Formatter>();\n\n    {\n        sFormatterMap.put(KEY_IJK_CODEC_LONG_NAME_UI, new Formatter() {\n            @Override\n            public String doFormat(IjkMediaFormat mediaFormat) {\n                return mMediaFormat.getString(IjkMediaMeta.IJKM_KEY_CODEC_LONG_NAME);\n            }\n        });\n        sFormatterMap.put(KEY_IJK_CODEC_NAME_UI, new Formatter() {\n            @Override\n            public String doFormat(IjkMediaFormat mediaFormat) {\n                return mMediaFormat.getString(IjkMediaMeta.IJKM_KEY_CODEC_NAME);\n            }\n        });\n        sFormatterMap.put(KEY_IJK_BIT_RATE_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int bitRate = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_BITRATE);\n                if (bitRate <= 0) {\n                    return null;\n                } else if (bitRate < 1000) {\n                    return String.format(Locale.US, \"%d bit/s\", bitRate);\n                } else {\n                    return String.format(Locale.US, \"%d kb/s\", bitRate / 1000);\n                }\n            }\n        });\n        sFormatterMap.put(KEY_IJK_CODEC_PROFILE_LEVEL_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int profileIndex = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_CODEC_PROFILE_ID);\n                String profile;\n                switch (profileIndex) {\n                    case IjkMediaMeta.FF_PROFILE_H264_BASELINE:\n                        profile = \"Baseline\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_CONSTRAINED_BASELINE:\n                        profile = \"Constrained Baseline\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_MAIN:\n                        profile = \"Main\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_EXTENDED:\n                        profile = \"Extended\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH:\n                        profile = \"High\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_10:\n                        profile = \"High 10\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_10_INTRA:\n                        profile = \"High 10 Intra\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_422:\n                        profile = \"High 4:2:2\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_422_INTRA:\n                        profile = \"High 4:2:2 Intra\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_444:\n                        profile = \"High 4:4:4\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_444_PREDICTIVE:\n                        profile = \"High 4:4:4 Predictive\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_HIGH_444_INTRA:\n                        profile = \"High 4:4:4 Intra\";\n                        break;\n                    case IjkMediaMeta.FF_PROFILE_H264_CAVLC_444:\n                        profile = \"CAVLC 4:4:4\";\n                        break;\n                    default:\n                        return null;\n                }\n\n                StringBuilder sb = new StringBuilder();\n                sb.append(profile);\n\n                String codecName = mediaFormat.getString(IjkMediaMeta.IJKM_KEY_CODEC_NAME);\n                if (!TextUtils.isEmpty(codecName) && codecName.equalsIgnoreCase(CODEC_NAME_H264)) {\n                    int level = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_CODEC_LEVEL);\n                    if (level < 10)\n                        return sb.toString();\n\n                    sb.append(\" Profile Level \");\n                    sb.append((level / 10) % 10);\n                    if ((level % 10) != 0) {\n                        sb.append(\".\");\n                        sb.append(level % 10);\n                    }\n                }\n\n                return sb.toString();\n            }\n        });\n        sFormatterMap.put(KEY_IJK_CODEC_PIXEL_FORMAT_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                return mediaFormat.getString(IjkMediaMeta.IJKM_KEY_CODEC_PIXEL_FORMAT);\n            }\n        });\n        sFormatterMap.put(KEY_IJK_RESOLUTION_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int width = mediaFormat.getInteger(KEY_WIDTH);\n                int height = mediaFormat.getInteger(KEY_HEIGHT);\n                int sarNum = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_SAR_NUM);\n                int sarDen = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_SAR_DEN);\n\n                if (width <= 0 || height <= 0) {\n                    return null;\n                } else if (sarNum <= 0 || sarDen <= 0) {\n                    return String.format(Locale.US, \"%d x %d\", width, height);\n                } else {\n                    return String.format(Locale.US, \"%d x %d [SAR %d:%d]\", width,\n                            height, sarNum, sarDen);\n                }\n            }\n        });\n        sFormatterMap.put(KEY_IJK_FRAME_RATE_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int fpsNum = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_FPS_NUM);\n                int fpsDen = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_FPS_DEN);\n                if (fpsNum <= 0 || fpsDen <= 0) {\n                    return null;\n                } else {\n                    return String.valueOf(((float) (fpsNum)) / fpsDen);\n                }\n            }\n        });\n        sFormatterMap.put(KEY_IJK_SAMPLE_RATE_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int sampleRate = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_SAMPLE_RATE);\n                if (sampleRate <= 0) {\n                    return null;\n                } else {\n                    return String.format(Locale.US, \"%d Hz\", sampleRate);\n                }\n            }\n        });\n        sFormatterMap.put(KEY_IJK_CHANNEL_UI, new Formatter() {\n            @Override\n            protected String doFormat(IjkMediaFormat mediaFormat) {\n                int channelLayout = mediaFormat.getInteger(IjkMediaMeta.IJKM_KEY_CHANNEL_LAYOUT);\n                if (channelLayout <= 0) {\n                    return null;\n                } else {\n                    if (channelLayout == IjkMediaMeta.AV_CH_LAYOUT_MONO) {\n                        return \"mono\";\n                    } else if (channelLayout == IjkMediaMeta.AV_CH_LAYOUT_STEREO) {\n                        return \"stereo\";\n                    } else {\n                        return String.format(Locale.US, \"%x\", channelLayout);\n                    }\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/misc/IjkTrackInfo.java",
    "content": "/*\n * Copyright (C) 2015 Bilibili\n * Copyright (C) 2015 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.misc;\n\nimport android.text.TextUtils;\n\nimport tv.danmaku.ijk.media.player.IjkMediaMeta;\n\npublic class IjkTrackInfo implements ITrackInfo {\n    private int mTrackType = MEDIA_TRACK_TYPE_UNKNOWN;\n    private IjkMediaMeta.IjkStreamMeta mStreamMeta;\n\n    public IjkTrackInfo(IjkMediaMeta.IjkStreamMeta streamMeta) {\n        mStreamMeta = streamMeta;\n    }\n\n    public void setMediaMeta(IjkMediaMeta.IjkStreamMeta streamMeta) {\n        mStreamMeta = streamMeta;\n    }\n\n    @Override\n    public IMediaFormat getFormat() {\n        return new IjkMediaFormat(mStreamMeta);\n    }\n\n    @Override\n    public String getLanguage() {\n        if (mStreamMeta == null || TextUtils.isEmpty(mStreamMeta.mLanguage))\n            return \"und\";\n\n        return mStreamMeta.mLanguage;\n    }\n\n    @Override\n    public int getTrackType() {\n        return mTrackType;\n    }\n\n    public void setTrackType(int trackType) {\n        mTrackType = trackType;\n    }\n\n    @Override\n    public String toString() {\n        return getClass().getSimpleName() + '{' + getInfoInline() + \"}\";\n    }\n\n    @Override\n    public String getInfoInline() {\n        StringBuilder out = new StringBuilder(128);\n        switch (mTrackType) {\n            case MEDIA_TRACK_TYPE_VIDEO:\n                out.append(\"VIDEO\");\n                out.append(\", \");\n                out.append(mStreamMeta.getCodecShortNameInline());\n                out.append(\", \");\n                out.append(mStreamMeta.getBitrateInline());\n                out.append(\", \");\n                out.append(mStreamMeta.getResolutionInline());\n                break;\n            case MEDIA_TRACK_TYPE_AUDIO:\n                out.append(\"AUDIO\");\n                out.append(\", \");\n                out.append(mStreamMeta.getCodecShortNameInline());\n                out.append(\", \");\n                out.append(mStreamMeta.getBitrateInline());\n                out.append(\", \");\n                out.append(mStreamMeta.getSampleRateInline());\n                break;\n            case MEDIA_TRACK_TYPE_TIMEDTEXT:\n                out.append(\"TIMEDTEXT\");\n                out.append(\", \");\n                out.append(mStreamMeta.mLanguage);\n                break;\n            case MEDIA_TRACK_TYPE_SUBTITLE:\n                out.append(\"SUBTITLE\");\n                break;\n            default:\n                out.append(\"UNKNOWN\");\n                break;\n        }\n        return out.toString();\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/pragma/DebugLog.java",
    "content": "/*\n * Copyright (C) 2013 Bilibili\n * Copyright (C) 2013 Zhang Rui <bbcallen@gmail.com>\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\npackage tv.danmaku.ijk.media.player.pragma;\n\nimport java.util.Locale;\n\n\nimport android.util.Log;\n\n@SuppressWarnings({\"SameParameterValue\", \"WeakerAccess\"})\npublic class DebugLog {\n    public static final boolean ENABLE_ERROR = Pragma.ENABLE_VERBOSE;\n    public static final boolean ENABLE_INFO = Pragma.ENABLE_VERBOSE;\n    public static final boolean ENABLE_WARN = Pragma.ENABLE_VERBOSE;\n    public static final boolean ENABLE_DEBUG = Pragma.ENABLE_VERBOSE;\n    public static final boolean ENABLE_VERBOSE = Pragma.ENABLE_VERBOSE;\n\n    public static void e(String tag, String msg) {\n        if (ENABLE_ERROR) {\n            Log.e(tag, msg);\n        }\n    }\n\n    public static void e(String tag, String msg, Throwable tr) {\n        if (ENABLE_ERROR) {\n            Log.e(tag, msg, tr);\n        }\n    }\n\n    public static void efmt(String tag, String fmt, Object... args) {\n        if (ENABLE_ERROR) {\n            String msg = String.format(Locale.US, fmt, args);\n            Log.e(tag, msg);\n        }\n    }\n\n    public static void i(String tag, String msg) {\n        if (ENABLE_INFO) {\n            Log.i(tag, msg);\n        }\n    }\n\n    public static void i(String tag, String msg, Throwable tr) {\n        if (ENABLE_INFO) {\n            Log.i(tag, msg, tr);\n        }\n    }\n\n    public static void ifmt(String tag, String fmt, Object... args) {\n        if (ENABLE_INFO) {\n            String msg = String.format(Locale.US, fmt, args);\n            Log.i(tag, msg);\n        }\n    }\n\n    public static void w(String tag, String msg) {\n        if (ENABLE_WARN) {\n            Log.w(tag, msg);\n        }\n    }\n\n    public static void w(String tag, String msg, Throwable tr) {\n        if (ENABLE_WARN) {\n            Log.w(tag, msg, tr);\n        }\n    }\n\n    public static void wfmt(String tag, String fmt, Object... args) {\n        if (ENABLE_WARN) {\n            String msg = String.format(Locale.US, fmt, args);\n            Log.w(tag, msg);\n        }\n    }\n\n    public static void d(String tag, String msg) {\n        if (ENABLE_DEBUG) {\n            Log.d(tag, msg);\n        }\n    }\n\n    public static void d(String tag, String msg, Throwable tr) {\n        if (ENABLE_DEBUG) {\n            Log.d(tag, msg, tr);\n        }\n    }\n\n    public static void dfmt(String tag, String fmt, Object... args) {\n        if (ENABLE_DEBUG) {\n            String msg = String.format(Locale.US, fmt, args);\n            Log.d(tag, msg);\n        }\n    }\n\n    public static void v(String tag, String msg) {\n        if (ENABLE_VERBOSE) {\n            Log.v(tag, msg);\n        }\n    }\n\n    public static void v(String tag, String msg, Throwable tr) {\n        if (ENABLE_VERBOSE) {\n            Log.v(tag, msg, tr);\n        }\n    }\n\n    public static void vfmt(String tag, String fmt, Object... args) {\n        if (ENABLE_VERBOSE) {\n            String msg = String.format(Locale.US, fmt, args);\n            Log.v(tag, msg);\n        }\n    }\n\n    public static void printStackTrace(Throwable e) {\n        if (ENABLE_WARN) {\n            e.printStackTrace();\n        }\n    }\n\n    public static void printCause(Throwable e) {\n        if (ENABLE_WARN) {\n            Throwable cause = e.getCause();\n            if (cause != null)\n                e = cause;\n\n            printStackTrace(e);\n        }\n    }\n}\n"
  },
  {
    "path": "ijkplayer/src/main/java/tv/danmaku/ijk/media/player/pragma/Pragma.java",
    "content": "/*\n * Copyright (C) 2013 Bilibili\n * Copyright (C) 2013 Zhang Rui <bbcallen@gmail.com>\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 */\npackage tv.danmaku.ijk.media.player.pragma;\n\n/*-\n * configurated by app project\n */\npublic class Pragma {\n    public static final boolean ENABLE_VERBOSE = true;\n}\n"
  },
  {
    "path": "ijkplayer/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">ijkplayerlib</string>\n</resources>\n"
  },
  {
    "path": "mediaproxy/.gitignore",
    "content": "mediaproxylib.iml\nbuild\nbuild/*\n/mediaproxy.iml\n"
  },
  {
    "path": "mediaproxy/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion \"28.0.2\"\n\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n    implementation project(path: ':base')\n    implementation project(path: ':androidasync')\n}\n"
  },
  {
    "path": "mediaproxy/mediaproxy.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":mediaproxy\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" external.system.module.group=\"MediaSDK\" external.system.module.version=\"unspecified\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":mediaproxy\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/src/debug/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/androidTest/res;file://$MODULE_DIR$/src/test/res;file://$MODULE_DIR$/src/androidTestDebug/res;file://$MODULE_DIR$/src/testDebug/res;file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug;file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_8\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 28 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.collection:collection:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-common:2.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.annotation:annotation:1.1.0@jar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.fragment:fragment:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.viewpager:viewpager:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.loader:loader:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.activity:activity:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.customview:customview:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.core:core:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.savedstate:savedstate:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.interpolator:interpolator:1.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Gradle: androidx.arch.core:core-runtime:2.0.0@aar\" level=\"project\" />\n    <orderEntry type=\"module\" module-name=\"base\" />\n    <orderEntry type=\"module\" module-name=\"androidasync\" />\n  </component>\n</module>"
  },
  {
    "path": "mediaproxy/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "mediaproxy/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.mediaproxylib\" />\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/CacheManager.java",
    "content": "package com.media.cache;\n\nimport com.android.baselib.utils.LogUtils;\n\nimport java.io.File;\n\npublic class CacheManager {\n\n    public static void deleteCacheFile() {\n        File file = new File(getCachePath());\n        LogUtils.w(\"deleteCacheFile file path = \" + file.getAbsolutePath());\n        deleteCacheFile(file);\n    }\n\n    private static void deleteCacheFile(File file) {\n        LogUtils.w(\"\"+file);\n        if (!file.exists()) {\n            return;\n        }\n        if (file.isDirectory()) {\n            File[] listFiles = file.listFiles();\n            for (File f : listFiles) {\n                if (f.isDirectory()) {\n                    deleteCacheFile(f);\n                    f.delete();\n                } else {\n                    f.delete();\n                }\n            }\n        } else {\n            file.delete();\n        }\n    }\n\n    public static String getCachedSize() {\n        File file = new File(getCachePath());\n        if (!file.exists()) {\n            return \"Null\";\n        }\n\n        long totalSize = getFileSize(file);\n\n        return totalSize / 1024 / 1024 + \" MB\";\n    }\n\n    private static long getFileSize(File file) {\n        if (file == null) {\n            return 0L;\n        }\n        long totalSize = 0L;\n        if (file.isDirectory()) {\n            File[] listFiles = file.listFiles();\n            for (File f : listFiles) {\n                if (f.isDirectory()) {\n                    totalSize += getFileSize(f);\n                } else {\n                    totalSize += f.length();\n                }\n            }\n        } else {\n            totalSize += file.length();\n        }\n        return totalSize;\n    }\n\n    public static String getCachePath() {\n        return VideoDownloadManager.getInstance().getCacheFilePath();\n    }\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/DownloadConstants.java",
    "content": "package com.media.cache;\n\npublic class DownloadConstants {\n  public static final int READ_TIMEOUT = 30 * 1000;\n  public static final int CONN_TIMEOUT = 30 * 1000;\n  public static final int SOCKET_TIMEOUT = 60 * 1000;\n  public static final int CONCURRENT_COUNT = 3;\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/LocalProxyConfig.java",
    "content": "package com.media.cache;\n\nimport android.content.Context;\n\nimport java.io.File;\n\npublic class LocalProxyConfig {\n\n    private boolean mIsDebug = false;\n    private Context mContext;\n    private File mCacheRoot;\n    private String mHost;\n    private int mPort;\n    private long mCacheSize;\n    private int mReadTimeOut;\n    private int mConnTimeOut;\n    private int mSocketTimeOut;\n    private boolean mRedirect;\n    private boolean mFlowControlEnable;\n    private long mMaxBufferSize;\n    private long mMinBufferSize;\n    private boolean mIgnoreAllCertErrors;\n    private int mConcurrentCount;\n\n    public LocalProxyConfig(Context context, File cacheRoot,\n                                 long cacheSize, int readTimeOut,\n                                 int connTimeOut, int socketTimeOut,\n                                 boolean redirect, boolean ignoreAllCertErrors, int port,\n                                 boolean flowControlEnable, long maxBufferSize,\n                                 long minBufferSize, int count) {\n        mContext = context;\n        mCacheRoot = cacheRoot;\n        mCacheSize = cacheSize;\n        mReadTimeOut = readTimeOut;\n        mConnTimeOut = connTimeOut;\n        mSocketTimeOut = socketTimeOut;\n        mRedirect = redirect;\n        mIgnoreAllCertErrors = ignoreAllCertErrors;\n        mPort = port;\n        mFlowControlEnable = flowControlEnable;\n        mMaxBufferSize = maxBufferSize;\n        mMinBufferSize = minBufferSize;\n        mConcurrentCount = count;\n    }\n\n    public Context getContext() { return mContext; }\n\n    public int getConnTimeOut() {\n        return mConnTimeOut;\n    }\n\n    public int getReadTimeOut() {\n        return mReadTimeOut;\n    }\n\n    public int getSocketTimeOut() { return mSocketTimeOut; }\n\n    public long getCacheSize() {\n        return mCacheSize;\n    }\n\n    public int getPort() {\n        return mPort;\n    }\n\n    public String getHost() {\n        return mHost;\n    }\n\n    public void setConfig(String host, int port) {\n        mHost = host;\n        mPort = port;\n    }\n\n    public File getCacheRoot() {\n        return mCacheRoot;\n    }\n\n    public boolean isRedirect() { return mRedirect; }\n\n    public void setFlowControlEnable(boolean enable) {\n        mFlowControlEnable = enable;\n    }\n\n    public boolean getFlowControlEnable() {\n        return mFlowControlEnable;\n    }\n\n    public long getMaxBufferSize() { return mMaxBufferSize; }\n\n    public long getMinBufferSize() { return mMinBufferSize; }\n\n    public boolean isDebug() { return mIsDebug; }\n\n    public void setIgnoreAllCertErrors(boolean enable) {\n        mIgnoreAllCertErrors = enable;\n    }\n\n    public boolean shouldIgnoreAllCertErrors() {\n        return mIgnoreAllCertErrors;\n    }\n\n    public void setConcurrentCount(int count) { mConcurrentCount = count; }\n\n    public int getConcurrentCount() { return mConcurrentCount; }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/StorageManager.java",
    "content": "package com.media.cache;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.utils.LocalProxyThreadUtils;\nimport com.media.cache.utils.LocalProxyUtils;\nimport com.media.cache.utils.StorageUtils;\n\nimport java.io.File;\nimport java.util.List;\nimport java.util.concurrent.Callable;\n\npublic class StorageManager {\n\n    private static StorageManager sInstance = null;\n\n    public static StorageManager getInstance() {\n        if (sInstance == null) {\n            synchronized (StorageManager.class) {\n                if (sInstance == null) {\n                    sInstance = new StorageManager();\n                }\n            }\n        }\n        return sInstance;\n    }\n\n\n    //1.Update cache file's last-modified-time.\n    //2.Get LRU files.\n    //3.Delete the files by LRU.\n    public void checkCacheFile(File saveDir, long limitCacheSize) {\n        try {\n            LocalProxyThreadUtils.submitCallbackTask(new CheckFileCallable(saveDir, limitCacheSize));\n        } catch (Exception e) {\n            LogUtils.w(\"VideoDownloadTask checkCacheFile \" + saveDir +\" failed, exception=\"+e);\n        }\n    }\n\n    private class CheckFileCallable implements Callable<Void> {\n\n        private File mDir;\n        private long mLimitCacheSize;\n\n        public CheckFileCallable(File dir, long cacheSize) {\n            mDir = dir;\n            mLimitCacheSize = cacheSize;\n        }\n\n        @Override\n        public Void call() throws Exception {\n            LocalProxyUtils.setLastModifiedNow(mDir);\n            trimCacheFile(mDir.getParentFile(), mLimitCacheSize);\n            return null;\n        }\n    }\n\n    private void trimCacheFile(File dir, long cacheSize) {\n        List<File> files = StorageUtils.getLruFileList(dir);\n        trimCacheFile(files, cacheSize);\n    }\n\n    private void trimCacheFile(List<File> files, long limitCacheSize) {\n        long totalSize = StorageUtils.countTotalSize(files);\n        int totalCount = files.size();\n        for (File file : files) {\n            boolean shouldDeleteFile = shouldDeleteFile(totalSize, totalCount, limitCacheSize);\n            if (shouldDeleteFile) {\n                long fileLength = StorageUtils.countTotalSize(file);\n                boolean deleted = StorageUtils.deleteFile(file);\n                if (deleted) {\n                    totalSize -= fileLength;\n                    totalCount--;\n                    LogUtils.i(\"trimCacheFile okay.\");\n                } else {\n                    LogUtils.w(\"trimCacheFile delete file \" + file.getAbsolutePath() +\" failed.\");\n                }\n            }\n        }\n    }\n\n    private boolean shouldDeleteFile(long totalSize, int totalCount, long limitCacheSize) {\n        if (totalCount <= 1) {\n            return false;\n        }\n        return totalSize > limitCacheSize;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/VideoCacheException.java",
    "content": "package com.media.cache;\n\npublic class VideoCacheException extends Exception {\n\n    private String mMsg;\n\n    public VideoCacheException(String msg) {\n        mMsg = msg;\n    }\n\n    public String getMsg() {\n        return mMsg;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/VideoDownloadManager.java",
    "content": "package com.media.cache;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.IntentFilter;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkRequest;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.text.TextUtils;\n\nimport com.android.baselib.MediaSDKReceiver;\nimport com.android.baselib.NetworkCallbackImpl;\nimport com.android.baselib.NetworkListener;\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.download.BaseVideoDownloadTask;\nimport com.media.cache.download.M3U8VideoDownloadTask;\nimport com.media.cache.download.VideoDownloadTask;\nimport com.media.cache.hls.M3U8;\nimport com.media.cache.listener.IDownloadInfosCallback;\nimport com.media.cache.listener.IDownloadListener;\nimport com.media.cache.listener.IVideoInfoCallback;\nimport com.media.cache.listener.IVideoInfoParseCallback;\nimport com.media.cache.listener.IDownloadTaskListener;\nimport com.media.cache.model.Video;\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.model.VideoTaskItem;\nimport com.media.cache.model.VideoTaskState;\nimport com.media.cache.proxy.AsyncProxyServer;\nimport com.media.cache.proxy.CustomProxyServer;\nimport com.media.cache.utils.DownloadExceptionUtils;\nimport com.media.cache.utils.LocalProxyUtils;\nimport com.media.cache.utils.StorageUtils;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\npublic class VideoDownloadManager {\n\n    private static final int MSG_DOWNLOAD_DEFAULT = 0;\n    private static final int MSG_DOWNLOAD_PENDING = 1;\n    private static final int MSG_DOWNLOAD_PREPARE = 2;\n    private static final int MSG_DOWNLOAD_START = 3;\n    private static final int MSG_DOWNLOAD_PROXY_READY = 4;\n    private static final int MSG_DOWNLOAD_PROCESSING = 5;\n    private static final int MSG_DOWNLOAD_SPEED = 6;\n    private static final int MSG_DOWNLOAD_PAUSE = 7;\n    private static final int MSG_DOWNLOAD_SUCCESS = 8;\n    private static final int MSG_DOWNLOAD_ERROR = 9;\n    private static final int MSG_DOWNLOAD_PROXY_FORBIDDEN = 10;\n\n    private static final int MSG_DOWNLOAD_INFOS = 100;\n\n    private static VideoDownloadManager sInstance = null;\n    private LocalProxyConfig mConfig;\n    private VideoDownloadQueue mVideoDownloadQueue;\n    private Handler mDownloadHandler = new DownloadHandler();\n    private IDownloadListener mGlobalDownloadListener;\n    private List<IDownloadInfosCallback> mDownloadInfoCallbacks = new CopyOnWriteArrayList<>();\n    private Map<String, VideoDownloadTask> mVideoDownloadTaskMap = new ConcurrentHashMap<>();\n    private Map<String, IDownloadListener> mDownloadListenerMap = new ConcurrentHashMap<>();\n\n    public static VideoDownloadManager getInstance() {\n        if (sInstance == null) {\n            synchronized (VideoDownloadManager.class) {\n                if (sInstance == null) {\n                    sInstance = new VideoDownloadManager();\n                }\n            }\n        }\n        return sInstance;\n    }\n\n    private VideoDownloadManager() {\n        mVideoDownloadQueue = new VideoDownloadQueue();\n    }\n\n    public LocalProxyConfig downloadConfig() { return mConfig; }\n\n    public void initConfig(LocalProxyConfig config) {\n        if (config == null)\n            return;\n        mConfig = config;\n        new CustomProxyServer(mConfig);\n        VideoInfoParserManager.getInstance().initConfig(config);\n        registerReceiver(mConfig.getContext());\n    }\n\n    public void registerReceiver(Context context) {\n        MediaSDKReceiver receiver = new MediaSDKReceiver();\n        context.registerReceiver(receiver, new IntentFilter(\"android.net.conn.CONNECTIVITY_CHANGE\"));\n    }\n\n    @SuppressLint(\"NewApi\")\n    @SuppressWarnings({\"MissingPermission\"})\n    private void registerConnectionListener(Context context) {\n        NetworkCallbackImpl networkCallback = new NetworkCallbackImpl(mNetworkListener);\n        NetworkRequest request = new NetworkRequest.Builder().build();\n        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n        if (manager != null) {\n            manager.registerNetworkCallback(request, networkCallback);\n        }\n    }\n\n    public String getCacheFilePath() {\n        if (mConfig != null) {\n            return mConfig.getCacheRoot().getAbsolutePath();\n        }\n        return null;\n    }\n\n    public VideoDownloadManager(LocalProxyConfig config) {\n        new AsyncProxyServer(config);\n        mConfig = config;\n    }\n\n    //1.DOWNLOAD MODULE\n    //-----------------------------------------------------------------------\n    //-------------------------DOWNLOAD MODULE-------------------------------\n    //-----------------------------------------------------------------------\n    public void fetchDownloadItems(IDownloadInfosCallback callback) {\n        mDownloadInfoCallbacks.add(callback);\n    }\n\n    public void removeDownloadInfosCallback(IDownloadInfosCallback callback) {\n        mDownloadInfoCallbacks.remove(callback);\n    }\n\n    public void setGlobalDownloadListener(IDownloadListener listener) {\n        mGlobalDownloadListener = listener;\n    }\n\n    public void startDownload(VideoTaskItem taskItem) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()) || taskItem.getUrl().startsWith(\"http://127.0.0.1\"))\n            return;\n\n        if (mVideoDownloadQueue.contains(taskItem)) {\n            taskItem = mVideoDownloadQueue.getTaskItem(taskItem.getUrl());\n        } else {\n            mVideoDownloadQueue.offer(taskItem);\n        }\n        taskItem.setTaskState(VideoTaskState.PENDING);\n        mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PENDING, taskItem).sendToTarget();\n\n        if (mVideoDownloadQueue.getDownloadingCount() < mConfig.getConcurrentCount()) {\n            startDownload(taskItem, null);\n        }\n    }\n\n    public void startDownload(VideoTaskItem taskItem, HashMap<String, String> headers) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()) || taskItem.getUrl().startsWith(\"http://127.0.0.1\"))\n            return;\n        taskItem.setTaskState(VideoTaskState.PREPARE);\n        mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PREPARE, taskItem).sendToTarget();\n        parseVideoInfo(taskItem, headers);\n    }\n\n    public void removeDownloadQueue(VideoTaskItem item) {\n        mVideoDownloadQueue.remove(item);\n        while(mVideoDownloadQueue.getDownloadingCount() < mConfig.getConcurrentCount() ) {\n            if (mVideoDownloadQueue.getDownloadingCount() == mVideoDownloadQueue.size())\n                break;\n            VideoTaskItem item1 = mVideoDownloadQueue.peekPendingTask();\n            startDownload(item1, null);\n        }\n\n    }\n\n    //Delete one task\n    public void deleteVideoTask(VideoTaskItem taskItem) {\n        String cacheFilePath = getCacheFilePath();\n        if (!TextUtils.isEmpty(cacheFilePath)) {\n            if (taskItem.isRunningTask()) {\n                pauseDownloadTask(taskItem);\n            }\n            String saveName = LocalProxyUtils.computeMD5(taskItem.getUrl());\n            File file = new File(cacheFilePath + File.separator + saveName);\n            StorageUtils.deleteCacheFile(file);\n            taskItem.setTaskState(VideoTaskState.DEFAULT);\n            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_DEFAULT, taskItem).sendToTarget();\n        }\n    }\n\n    public void deleteVideoTasks(VideoTaskItem[] taskItems) {\n        String cacheFilePath = getCacheFilePath();\n        if (!TextUtils.isEmpty(cacheFilePath)) {\n            for (VideoTaskItem item : taskItems) {\n                deleteVideoTask(item);\n            }\n        }\n    }\n\n    //Delete all files\n    public void deleteAllVideoFiles(Context context) {\n        try {\n            StorageUtils.clearVideoCacheDir(context);\n        } catch (Exception e) {\n            LogUtils.w(\"clearVideoCacheDir failed, exception = \" + e.getMessage());\n        }\n    }\n\n    //Pause all download task\n    public void pauseDownloadTasks(VideoTaskItem[] taskItems) {\n        for (VideoTaskItem item : taskItems) {\n            if (item.isRunningTask()) {\n                pauseDownloadTask(item);\n            }\n        }\n    }\n    //-----------------------------------------------------------------------\n    //-------------------------DOWNLOAD MODULE-------------------------------\n    //-----------------------------------------------------------------------\n\n\n    //2.PLAY MODULE\n    //-----------------------------------------------------------------------\n    //-----------------------------PLAY MODULE-------------------------------\n    //-----------------------------------------------------------------------\n    public void startPlayCacheTask(VideoTaskItem taskItem, IDownloadListener listener) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()) || taskItem.getUrl().startsWith(\"http://127.0.0.1\"))\n            return;\n\n        startPlayCacheTask(taskItem, null, listener);\n    }\n\n    public void startPlayCacheTask(VideoTaskItem taskItem, HashMap<String, String> headers, IDownloadListener listener) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()) || taskItem.getUrl().startsWith(\"http://127.0.0.1\"))\n            return;\n        addCallback(taskItem.getUrl(), listener);\n        taskItem.setTaskState(VideoTaskState.PREPARE);\n        mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PREPARE, taskItem).sendToTarget();\n        parseVideoInfo(taskItem, headers);\n    }\n    //-----------------------------------------------------------------------\n    //-----------------------------PLAY MODULE-------------------------------\n    //-----------------------------------------------------------------------\n\n\n    private void parseVideoInfo(VideoTaskItem taskItem, final HashMap<String, String> headers) {\n        String videoUrl = taskItem.getUrl();\n        String saveName = LocalProxyUtils.computeMD5(videoUrl);\n        VideoCacheInfo cacheInfo = LocalProxyUtils.readProxyCacheInfo(new File(mConfig.getCacheRoot(), saveName));\n        if (cacheInfo != null) {\n            LogUtils.w(\"parseVideoInfo info = \" + cacheInfo);\n            if (taskItem.isDownloadMode()) {\n                long createTime = cacheInfo.getDownloadTime();\n                if (createTime == 0L) {\n                    createTime = System.currentTimeMillis();\n                    cacheInfo.setDownloadTime(createTime);\n                    taskItem.setDownloadTime(createTime);\n                } else {\n                    taskItem.setDownloadTime(createTime);\n                }\n            }\n\n            if (cacheInfo.getVideoType() == Video.Type.MP4_TYPE\n                    || cacheInfo.getVideoType() == Video.Type.WEBM_TYPE\n                    || cacheInfo.getVideoType() == Video.Type.QUICKTIME_TYPE\n                    || cacheInfo.getVideoType() == Video.Type.GP3_TYPE) {\n                startBaseVideoDownloadTask(taskItem, cacheInfo, headers);\n            } else if (cacheInfo.getVideoType() == Video.Type.HLS_TYPE) {\n                VideoInfoParserManager.getInstance()\n                        .parseM3U8File(cacheInfo, new IVideoInfoParseCallback() {\n\n                            @Override\n                            public void onM3U8FileParseSuccess(VideoCacheInfo info, M3U8 m3u8) {\n                                startM3U8VideoDownloadTask(taskItem, info, m3u8, headers);\n                            }\n\n                            @Override\n                            public void onM3U8FileParseFailed(VideoCacheInfo info, Throwable error) {\n                                parseVideoInfo(taskItem, info, headers);\n                            }\n                        });\n            }\n        } else {\n            cacheInfo = new VideoCacheInfo(videoUrl);\n            cacheInfo.setTaskMode(taskItem.getTaskMode());\n            if (taskItem.isDownloadMode()) {\n                long createTime = System.currentTimeMillis();\n                cacheInfo.setDownloadTime(createTime);\n                taskItem.setDownloadTime(createTime);\n            }\n            parseVideoInfo(taskItem, cacheInfo, headers);\n        }\n    }\n\n    private void parseVideoInfo(VideoTaskItem taskItem, final VideoCacheInfo cacheInfo, final HashMap<String, String> headers) {\n        VideoInfoParserManager.getInstance().parseVideoInfo(cacheInfo, new IVideoInfoCallback() {\n            @Override\n            public void onFinalUrl(String finalUrl) {\n                //Get final url by redirecting.\n            }\n\n            @Override\n            public void onBaseVideoInfoSuccess(VideoCacheInfo cacheInfo) {\n                startBaseVideoDownloadTask(taskItem, cacheInfo, headers);\n            }\n\n            @Override\n            public void onBaseVideoInfoFailed(Throwable error) {\n                LogUtils.w(\"onInfoFailed error=\" +error);\n                int errorCode = DownloadExceptionUtils.getErrorCode(error);\n                taskItem.setErrorCode(errorCode);\n                taskItem.setTaskState(VideoTaskState.ERROR);\n                mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROXY_FORBIDDEN, taskItem).sendToTarget();\n            }\n\n            @Override\n            public void onM3U8InfoSuccess(VideoCacheInfo cacheInfo, M3U8 m3u8) {\n                startM3U8VideoDownloadTask(taskItem, cacheInfo, m3u8, headers);\n            }\n\n            @Override\n            public void onLiveM3U8Callback(VideoCacheInfo info) {\n                LogUtils.i(\"onLiveM3U8Callback cannot be cached.\");\n                taskItem.setTaskState(VideoTaskState.ERROR);\n                mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROXY_FORBIDDEN, taskItem).sendToTarget();\n            }\n\n            @Override\n            public void onM3U8InfoFailed(Throwable error) {\n                error.printStackTrace();\n                LogUtils.w(\"onM3U8InfoFailed : \" + error);\n                int errorCode = DownloadExceptionUtils.getErrorCode(error);\n                taskItem.setErrorCode(errorCode);\n                taskItem.setTaskState(VideoTaskState.ERROR);\n                mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROXY_FORBIDDEN, taskItem).sendToTarget();\n            }\n        }, headers);\n    }\n\n    public void startBaseVideoDownloadTask(VideoTaskItem taskItem,\n                                           VideoCacheInfo cacheInfo,\n                                           HashMap<String, String> headers) {\n        taskItem.setVideoType(cacheInfo.getVideoType());\n        VideoDownloadTask downloadTask = null;\n        if (!mVideoDownloadTaskMap.containsKey(cacheInfo.getUrl())) {\n            downloadTask = new BaseVideoDownloadTask(mConfig, cacheInfo, headers);\n            mVideoDownloadTaskMap.put(cacheInfo.getUrl(), downloadTask);\n        } else {\n            downloadTask = mVideoDownloadTaskMap.get(cacheInfo.getUrl());\n        }\n\n        if (downloadTask != null) {\n            downloadTask.startDownload(\n                    new IDownloadTaskListener() {\n\n                        @Override\n                        public void onTaskStart(String url) {\n                            taskItem.setTaskState(VideoTaskState.START);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_START, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onLocalProxyReady(String proxyUrl) {\n                            taskItem.setProxyUrl(proxyUrl);\n                            taskItem.setTaskState(VideoTaskState.PROXYREADY);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROXY_READY, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskProgress(float percent, long cachedSize, M3U8 m3u8) {\n                            if (taskItem.getTaskState() == VideoTaskState.PAUSE || taskItem.getTaskState() == VideoTaskState.SUCCESS) {\n                            } else {\n                                taskItem.setTaskState(VideoTaskState.DOWNLOADING);\n                                taskItem.setPercent(percent);\n                                taskItem.setDownloadSize(cachedSize);\n                                taskItem.setM3U8(m3u8);\n                                mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROCESSING, taskItem).sendToTarget();\n                            }\n                        }\n\n                        @Override\n                        public void onTaskSpeedChanged(float speed) {\n                            taskItem.setSpeed(speed);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_SPEED, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskPaused() {\n                            taskItem.setTaskState(VideoTaskState.PAUSE);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PAUSE, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskFinished(long totalSize) {\n                            taskItem.setTaskState(VideoTaskState.SUCCESS);\n                            taskItem.setDownloadSize(totalSize);\n                            taskItem.setPercent(100f);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_SUCCESS, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskFailed(Throwable e) {\n                            int errorCode = DownloadExceptionUtils.getErrorCode(e);\n                            taskItem.setErrorCode(errorCode);\n                            taskItem.setTaskState(VideoTaskState.ERROR);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_ERROR, taskItem).sendToTarget();\n                        }\n                    });\n        }\n\n    }\n\n    public void startM3U8VideoDownloadTask(VideoTaskItem taskItem,\n                                           VideoCacheInfo cacheInfo,\n                                           M3U8 m3u8,\n                                           HashMap<String, String> headers) {\n        taskItem.setVideoType(cacheInfo.getVideoType());\n        VideoDownloadTask downloadTask = null;\n        if (!mVideoDownloadTaskMap.containsKey(cacheInfo.getUrl())) {\n            downloadTask = new M3U8VideoDownloadTask(mConfig, cacheInfo, m3u8, headers);\n            mVideoDownloadTaskMap.put(cacheInfo.getUrl(), downloadTask);\n        } else {\n            downloadTask = mVideoDownloadTaskMap.get(cacheInfo.getUrl());\n        }\n\n        if (downloadTask != null) {\n            downloadTask.startDownload(\n                    new IDownloadTaskListener() {\n                        @Override\n                        public void onTaskStart(String url) {\n                            taskItem.setTaskState(VideoTaskState.START);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_START, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onLocalProxyReady(String proxyUrl) {\n                            taskItem.setProxyUrl(proxyUrl);\n                            taskItem.setTaskState(VideoTaskState.PROXYREADY);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROXY_READY, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskProgress(float percent, long cachedSize, M3U8 m3u8) {\n                            if (taskItem.getTaskState() == VideoTaskState.PAUSE || taskItem.getTaskState() == VideoTaskState.SUCCESS) {\n\n                            } else {\n                                taskItem.setTaskState(VideoTaskState.DOWNLOADING);\n                                taskItem.setPercent(percent);\n                                taskItem.setDownloadSize(cachedSize);\n                                taskItem.setM3U8(m3u8);\n                                mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PROCESSING, taskItem).sendToTarget();\n                            }\n                        }\n\n                        @Override\n                        public void onTaskSpeedChanged(float speed) {\n                            taskItem.setSpeed(speed);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_SPEED, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskPaused() {\n                            taskItem.setTaskState(VideoTaskState.PAUSE);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_PAUSE, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskFinished(long totalSize) {\n                            taskItem.setTaskState(VideoTaskState.SUCCESS);\n                            taskItem.setPercent(100f);\n                            taskItem.setDownloadSize(totalSize);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_SUCCESS, taskItem).sendToTarget();\n                        }\n\n                        @Override\n                        public void onTaskFailed(Throwable e) {\n                            int errorCode = DownloadExceptionUtils.getErrorCode(e);\n                            taskItem.setErrorCode(errorCode);\n                            taskItem.setTaskState(VideoTaskState.ERROR);\n                            mDownloadHandler.obtainMessage(MSG_DOWNLOAD_ERROR, taskItem).sendToTarget();\n                        }\n                    });\n        }\n\n    }\n\n    public void seekToDownloadTask(long curPosition, long totalDuration, String url) {\n        VideoDownloadTask task = mVideoDownloadTaskMap.get(url);\n        if (task != null) {\n            task.seekToDownload(curPosition, totalDuration);\n        }\n    }\n\n    public void seekToDownloadTask(float seekPercent, String url) {\n        VideoDownloadTask task = mVideoDownloadTaskMap.get(url);\n        if (task != null) {\n            task.seekToDownload(seekPercent);\n        }\n    }\n\n    public void resumeDownloadTask(VideoTaskItem taskItem, IDownloadListener listener) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()))\n            return;\n        String url = taskItem.getUrl();\n        VideoDownloadTask task = mVideoDownloadTaskMap.get(url);\n        if (task != null) {\n            task.resumeDownload();\n            addCallback(url, listener);\n        }\n    }\n\n    public void pauseDownloadTask(VideoTaskItem taskItem) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()))\n            return;\n        String url = taskItem.getUrl();\n        VideoDownloadTask task = mVideoDownloadTaskMap.get(url);\n        if (task != null) {\n            task.pauseDownload();\n        }\n    }\n\n    public void stopDownloadTask(VideoTaskItem taskItem) {\n        if (taskItem == null || TextUtils.isEmpty(taskItem.getUrl()))\n            return;\n        String url = taskItem.getUrl();\n        VideoDownloadTask task = mVideoDownloadTaskMap.get(url);\n        if (task != null) {\n            task.stopDownload();\n            mVideoDownloadTaskMap.remove(url);\n            removeCallback(url);\n        }\n    }\n\n    public void addCallback(String url, IDownloadListener listener){\n        if (TextUtils.isEmpty(url))\n            return;\n        mDownloadListenerMap.put(url, listener);\n    }\n\n    public void removeCallback(String url){\n        if (TextUtils.isEmpty(url))\n            return;\n        mDownloadListenerMap.remove(url);\n    }\n\n    class DownloadHandler extends Handler {\n\n        public DownloadHandler() {\n            super(Looper.getMainLooper());\n        }\n\n        @Override\n        public void handleMessage(Message msg) {\n            super.handleMessage(msg);\n            if (msg.what == MSG_DOWNLOAD_INFOS) {\n                dispatchDownloadInfos(msg.what, msg.obj);\n            } else {\n                dispatchVideoCacheState(msg.what, msg.obj);\n            }\n        }\n\n        private void dispatchDownloadInfos(int msg, Object obj) {\n\n        }\n\n        private void dispatchVideoCacheState(int msg, Object obj) {\n            VideoTaskItem item = (VideoTaskItem)obj;\n                IDownloadListener listener = mDownloadListenerMap.containsKey(item.getUrl()) ?\n                        mDownloadListenerMap.get(item.getUrl()) : null;\n                handleMessage(msg, item, listener);\n                handleMessage(msg, item, mGlobalDownloadListener);\n        }\n\n        private void handleMessage(int msg, VideoTaskItem item, IDownloadListener listener) {\n            if (listener != null) {\n                switch (msg) {\n                    case MSG_DOWNLOAD_DEFAULT:\n                        listener.onDownloadDefault(item);\n                        break;\n                    case MSG_DOWNLOAD_PENDING:\n                        listener.onDownloadPending(item);\n                        break;\n                    case MSG_DOWNLOAD_PREPARE:\n                        listener.onDownloadPrepare(item);\n                        break;\n                    case MSG_DOWNLOAD_START:\n                        listener.onDownloadStart(item);\n                        break;\n                    case MSG_DOWNLOAD_PROXY_READY:\n                        listener.onDownloadProxyReady(item);\n                        break;\n                    case MSG_DOWNLOAD_PROCESSING:\n                        listener.onDownloadProgress(item);\n                        break;\n                    case MSG_DOWNLOAD_SPEED:\n                        listener.onDownloadSpeed(item);\n                        break;\n                    case MSG_DOWNLOAD_PAUSE:\n                        removeDownloadQueue(item);\n                        listener.onDownloadPause(item);\n                        break;\n                    case MSG_DOWNLOAD_PROXY_FORBIDDEN:\n                        removeDownloadQueue(item);\n                        listener.onDownloadProxyForbidden(item);\n                        break;\n                    case MSG_DOWNLOAD_ERROR:\n                        removeDownloadQueue(item);\n                        listener.onDownloadError(item);\n                        break;\n                    case MSG_DOWNLOAD_SUCCESS:\n                        removeDownloadQueue(item);\n                        listener.onDownloadSuccess(item);\n                        break;\n                }\n            }\n        }\n    }\n\n    private NetworkListener mNetworkListener = new NetworkListener() {\n        @Override\n        public void onAvailable() {\n            LogUtils.e(\"onAvailable\");\n        }\n\n        @Override\n        public void onWifiConnected() {\n            LogUtils.e(\"onWifiConnected\");\n        }\n\n        @Override\n        public void onMobileConnected() {\n            LogUtils.e(\"onMobileConnected\");\n        }\n\n        @Override\n        public void onNetworkType() {\n            LogUtils.e(\"onNetworkType\");\n        }\n\n        @Override\n        public void onUnConnected() {\n            LogUtils.e(\"onUnConnected\");\n        }\n    };\n\n    public static class Build {\n        private Context mContext;\n        private File mCacheRoot;\n        private long mCacheSize = 2 * 1024 * 1024 * 1024L;  // Default 2G.\n        private int mReadTimeOut = 30 * 1000;    // 30 seconds\n        private int mConnTimeOut = 30 * 1000;    // 30 seconds\n        private int mSocketTimeOut = 60 * 1000; // 60 seconds\n        private boolean mRedirect = false;\n        private boolean mIgnoreAllCertErrors = false;\n        private int mConcurrentCount = 3;\n        private int mPort;\n        private boolean mFlowControlEnable = false; // true: control flow; false: no control\n        private long mMaxBufferSize = 20 * 1024 * 1024L;  // 20M\n        private long mMinBufferSize = 10 * 1024 * 1024L;  // 10M\n\n        public Build(Context context) {\n            mContext = context;\n        }\n\n        public Build setCacheRoot(File cacheRoot) {\n            mCacheRoot = cacheRoot;\n            return this;\n        }\n\n        public Build setCacheSize(long cacheSize) {\n            mCacheSize = cacheSize;\n            return this;\n        }\n\n        public Build setTimeOut(int readTimeOut, int connTimeOut, int socketTimeOut) {\n            mReadTimeOut = readTimeOut;\n            mConnTimeOut = connTimeOut;\n            mSocketTimeOut = socketTimeOut;\n            return this;\n        }\n\n        public Build setUrlRedirect(boolean redirect) {\n            mRedirect = redirect;\n            return this;\n        }\n\n        public Build setConcurrentCount(int count) {\n            mConcurrentCount = count;\n            return this;\n        }\n\n        public Build setIgnoreAllCertErrors(boolean ignoreAllCertErrors) {\n            mIgnoreAllCertErrors = ignoreAllCertErrors;\n            return this;\n        }\n\n        public Build setPort(int port) {\n            mPort = port;\n            return this;\n        }\n\n        //You can set enable=true to control flow.\n        public Build setFlowControlEnable(boolean enable) {\n            mFlowControlEnable = enable;\n            return this;\n        }\n\n        //You can set maxBufferSize and minBufferSize when in mobile state.\n        public Build setBufferSize(long maxBufferSize, long minBufferSize) {\n            mMaxBufferSize = maxBufferSize;\n            mMinBufferSize = minBufferSize;\n            return this;\n        }\n\n        public VideoDownloadManager build() {\n            return new VideoDownloadManager(buildConfig());\n        }\n\n        public LocalProxyConfig buildConfig() {\n            return new LocalProxyConfig(mContext, mCacheRoot, mCacheSize,\n                    mReadTimeOut, mConnTimeOut, mSocketTimeOut, mRedirect,\n                    mIgnoreAllCertErrors, mPort, mFlowControlEnable,\n                    mMaxBufferSize, mMinBufferSize, mConcurrentCount);\n        }\n    }\n\n    public void setIgnoreAllCertErrors(boolean enable) {\n        if (mConfig != null) {\n            mConfig.setIgnoreAllCertErrors(enable);\n        }\n    }\n\n    public void setConcurrentCount(int count) {\n        if (mConfig != null) {\n            mConfig.setConcurrentCount(count);\n        }\n    }\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/VideoDownloadQueue.java",
    "content": "package com.media.cache;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.model.VideoTaskItem;\nimport com.media.cache.model.VideoTaskState;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Custom Download Queue.\n * Non-thread safe\n */\npublic class VideoDownloadQueue {\n\n    private List<VideoTaskItem> mQueue;\n\n    public VideoDownloadQueue() {\n        mQueue = new ArrayList<>();\n    }\n\n    //put it into queue\n    public void offer(VideoTaskItem item) {\n        mQueue.add(item);\n    }\n\n    //Remove Queue head item,\n    //Return Next Queue head.\n    public VideoTaskItem poll() {\n        try {\n            if (mQueue.size() >= 2) {\n                mQueue.remove(0);\n                return mQueue.get(0);\n            } else if (mQueue.size() == 1) {\n                mQueue.remove(0);\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"DownloadQueue remove failed.\");\n        }\n        return null;\n    }\n\n    public VideoTaskItem peek() {\n        try {\n            if (mQueue.size() >= 1) {\n                return mQueue.get(0);\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"DownloadQueue get failed.\");\n        }\n        return null;\n    }\n\n    public boolean remove(VideoTaskItem item) {\n        if (contains(item)) {\n            return mQueue.remove(item);\n        }\n        return false;\n    }\n\n    public boolean contains(VideoTaskItem item) {\n        return mQueue.contains(item);\n    }\n\n    public VideoTaskItem getTaskItem(String url) {\n        try {\n            for (int index = 0; index < mQueue.size(); index++) {\n                if (mQueue.get(index).getUrl().equals(url)) {\n                    return mQueue.get(index);\n                }\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"DownloadQueue getTaskItem failed.\");\n        }\n        return null;\n    }\n\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    public int size() {\n        return mQueue.size();\n    }\n\n    public boolean isHead(VideoTaskItem item) {\n        return item.equals(peek());\n    }\n\n    public int getDownloadingCount() {\n        int count = 0;\n        try {\n            for (int index = 0; index < mQueue.size(); index++) {\n                if (isTaskRunnig(mQueue.get(index))) {\n                    count++;\n                }\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"DownloadQueue getDownloadingCount failed.\");\n        }\n        return count;\n    }\n\n    public VideoTaskItem peekPendingTask() {\n        try {\n            for (int index = 0; index < mQueue.size(); index++) {\n                VideoTaskItem item = mQueue.get(index);\n                if (isTaskPending(item)) {\n                    return item;\n                }\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"DownloadQueue getDownloadingCount failed.\");\n        }\n        return null;\n    }\n\n    public boolean isTaskPending(VideoTaskItem item) {\n        int taskState = item.getTaskState();\n        return taskState == VideoTaskState.PENDING;\n    }\n\n    public boolean isTaskRunnig(VideoTaskItem item) {\n        int taskState = item.getTaskState();\n        return taskState == VideoTaskState.PREPARE\n                || taskState == VideoTaskState.START\n                || taskState == VideoTaskState.DOWNLOADING\n                || taskState == VideoTaskState.PROXYREADY;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/VideoInfoParserManager.java",
    "content": "package com.media.cache;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.hls.M3U8;\nimport com.media.cache.hls.M3U8Utils;\nimport com.media.cache.listener.IVideoInfoCallback;\nimport com.media.cache.listener.IVideoInfoParseCallback;\nimport com.media.cache.model.Video;\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.utils.DownloadExceptionUtils;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyThreadUtils;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.File;\nimport java.util.HashMap;\n\npublic class VideoInfoParserManager {\n\n    private static VideoInfoParserManager sInstance;\n    private LocalProxyConfig mConfig;\n\n    public static VideoInfoParserManager getInstance() {\n        if (sInstance == null) {\n            synchronized (VideoInfoParserManager.class) {\n                if (sInstance == null) {\n                    sInstance = new VideoInfoParserManager();\n                }\n            }\n        }\n        return sInstance;\n    }\n\n    public void initConfig(LocalProxyConfig config) {\n        mConfig = config;\n    }\n\n    public synchronized void parseVideoInfo(VideoCacheInfo info, IVideoInfoCallback callback, HashMap<String, String> headers) {\n        LocalProxyThreadUtils.submitRunnableTask(() -> doParseVideoInfoTask(info, callback, headers));\n    }\n\n    private void doParseVideoInfoTask(VideoCacheInfo info, IVideoInfoCallback callback, HashMap<String, String> headers) {\n        try {\n            if (info == null) {\n                callback.onBaseVideoInfoFailed(new Throwable(\"Video info is null.\"));\n                return;\n            }\n            if (!HttpUtils.matchHttpSchema(info.getUrl())) {\n                callback.onBaseVideoInfoFailed(new Throwable(\"Can parse the request resource's schema.\"));\n                return;\n            }\n\n            String finalUrl = info.getUrl();\n            LogUtils.d(\"doParseVideoInfoTask finalUrl=\"+finalUrl);\n            //Redirect is enabled, send redirect request to get final location.\n            if (mConfig.isRedirect()) {\n                finalUrl = HttpUtils.getFinalUrl(mConfig, info.getUrl(), headers);\n                if (TextUtils.isEmpty(finalUrl)) {\n                    callback.onBaseVideoInfoFailed(new Throwable(\"FinalUrl is null.\"));\n                    return;\n                }\n                callback.onFinalUrl(finalUrl);\n            }\n            info.setFinalUrl(finalUrl);\n\n            Uri uri = Uri.parse(finalUrl);\n            String fileName = uri.getLastPathSegment();\n            LogUtils.d(\"parseVideoInfo  fileName = \" + fileName);\n            //By suffix name.\n            if (fileName != null) {\n                fileName = fileName.toLowerCase();\n                if (fileName.endsWith(\".m3u8\")) {\n                    parseM3U8Info(info, callback, headers);\n                    return;\n                } else if (fileName.endsWith(\".mp4\")) {\n                    LogUtils.i(\"parseVideoInfo MP4_TYPE\");\n                    info.setVideoType(Video.Type.MP4_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                    return;\n                } else if (fileName.endsWith(\".mov\")) {\n                    LogUtils.i(\"parseVideoInfo QUICKTIME_TYPE\");\n                    info.setVideoType(Video.Type.QUICKTIME_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                    return;\n                } else if (fileName.endsWith(\".webm\")) {\n                    LogUtils.i(\"parseVideoInfo WEBM_TYPE\");\n                    info.setVideoType(Video.Type.WEBM_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                    return;\n                } else if (fileName.endsWith(\".3gp\")) {\n                    LogUtils.i(\"parseVideoInfo GP3_TYPE\");\n                    info.setVideoType(Video.Type.GP3_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                    return;\n                }\n            }\n            String mimeType = null;\n\n            //Add more video mimeType.\n            mimeType = HttpUtils.getMimeType(mConfig, finalUrl, headers);\n            LogUtils.i(\"parseVideoInfo mimeType=\"+mimeType);\n            if (mimeType != null) {\n                mimeType = mimeType.toLowerCase();\n                if (mimeType.contains(Video.Mime.MIME_TYPE_MP4)) {\n                    LogUtils.i(\"parseVideoInfo MP4_TYPE\");\n                    info.setVideoType(Video.Type.MP4_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                } else if (isM3U8Mimetype(mimeType)) {\n                    parseM3U8Info(info, callback, headers);\n                } else if (mimeType.contains(Video.Mime.MIME_TYPE_WEBM)) {\n                    LogUtils.i(\"parseVideoInfo QUICKTIME_TYPE\");\n                    info.setVideoType(Video.Type.WEBM_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                } else if (mimeType.contains(Video.Mime.MIME_TYPE_QUICKTIME)) {\n                    LogUtils.i(\"parseVideoInfo WEBM_TYPE\");\n                    info.setVideoType(Video.Type.QUICKTIME_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                } else if (mimeType.contains(Video.Mime.MIME_TYPE_3GP)) {\n                    LogUtils.i(\"parseVideoInfo GP3_TYPE\");\n                    info.setVideoType(Video.Type.GP3_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                } else if (mimeType.contains(Video.Mime.MIME_TYPE_MP3)){\n                    info.setVideoType(Video.Type.MP3_TYPE);\n                    callback.onBaseVideoInfoSuccess(info);\n                } else {\n                    callback.onBaseVideoInfoFailed(new VideoCacheException(DownloadExceptionUtils.MIMETYPE_NOT_FOUND_STRING));\n                }\n            } else {\n                callback.onBaseVideoInfoFailed(new VideoCacheException(DownloadExceptionUtils.MIMETYPE_NULL_ERROR_STRING));\n            }\n        } catch (Exception e) {\n            callback.onBaseVideoInfoFailed(e);\n        }\n    }\n\n    private boolean isM3U8Mimetype(String mimeType) {\n        return mimeType.contains(Video.Mime.MIME_TYPE_M3U8_1)\n                || mimeType.contains(Video.Mime.MIME_TYPE_M3U8_2)\n                || mimeType.contains(Video.Mime.MIME_TYPE_M3U8_3)\n                || mimeType.contains(Video.Mime.MIME_TYPE_M3U8_4);\n    }\n\n    private void parseM3U8Info(VideoCacheInfo info, IVideoInfoCallback callback, HashMap<String, String> headers) {\n        try {\n            M3U8 m3u8 = M3U8Utils.parseM3U8Info(mConfig, info.getUrl(), false, null);\n            //HLS LIVE video cannot be proxy cached.\n            if (m3u8.hasEndList()) {\n                String saveName = LocalProxyUtils.computeMD5(info.getUrl());\n                File dir = new File(mConfig.getCacheRoot(), saveName);\n                if (!dir.exists()) {\n                    dir.mkdir();\n                }\n                M3U8Utils.createRemoteM3U8(dir, m3u8);\n\n                info.setSaveDir(dir.getAbsolutePath());\n                info.setVideoType(Video.Type.HLS_TYPE);\n                callback.onM3U8InfoSuccess(info, m3u8);\n            } else {\n                info.setVideoType(Video.Type.HLS_LIVE_TYPE);\n                callback.onLiveM3U8Callback(info);\n            }\n        } catch (Exception e) {\n            callback.onM3U8InfoFailed(e);\n        }\n    }\n\n    public void parseM3U8File(VideoCacheInfo info, IVideoInfoParseCallback callback) {\n        File remoteM3U8File = new File(info.getSaveDir(), \"remote.m3u8\");\n        if (!remoteM3U8File.exists()) {\n            callback.onM3U8FileParseFailed(info, new Throwable(\"Cannot find remote.m3u8 file.\"));\n            return;\n        }\n        try {\n            M3U8 m3u8 = M3U8Utils.parseM3U8Info(mConfig, info.getUrl(), true, remoteM3U8File);\n            callback.onM3U8FileParseSuccess(info, m3u8);\n        } catch (Exception e) {\n            callback.onM3U8FileParseFailed(info, e);\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/download/BaseVideoDownloadTask.java",
    "content": "package com.media.cache.download;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.StorageManager;\nimport com.media.cache.VideoCacheException;\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.listener.IDownloadTaskListener;\nimport com.media.cache.utils.DownloadExceptionUtils;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyThreadUtils;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InterruptedIOException;\nimport java.io.RandomAccessFile;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\nimport javax.net.ssl.HttpsURLConnection;\n\nimport androidx.annotation.Nullable;\n\npublic class BaseVideoDownloadTask extends VideoDownloadTask {\n\n    private static final String VIDEO_SUFFIX = \".video\";\n\n    private final LinkedHashMap<Long, Long> mSegmentList;\n    private LinkedHashMap<Long, VideoRange> mVideoRangeMap;\n    private VideoRange mCurDownloadRange;\n    private long mTotalLength = -1L;\n\n    class VideoRange {\n\n        long start; //segment start.\n        long end;   //segment end.\n\n        VideoRange(long start, long end) {\n            this.start = start;\n            this.end = end;\n        }\n\n        @Override\n        public boolean equals(@Nullable Object obj) {\n            VideoRange range = (VideoRange)obj;\n            return (start == range.start) && (end == range.end);\n        }\n\n        public String toString() {\n            return \"VideoRange[start=\"+start+\", end=\"+end+\"]\";\n        }\n    }\n\n    public BaseVideoDownloadTask(LocalProxyConfig config,\n                                 VideoCacheInfo info,\n                                 HashMap<String, String> headers) {\n        super(config, info, headers);\n        this.mTotalLength = info.getTotalLength();\n        this.mSegmentList = mInfo.getSegmentList();\n        this.mVideoRangeMap = new LinkedHashMap<>();\n        mCurDownloadRange = new VideoRange(Long.MIN_VALUE, Long.MAX_VALUE);\n        initSegements();\n    }\n\n    private void initSegements() {\n        Iterator iterator = mSegmentList.entrySet().iterator();\n        LogUtils.i( \"initSegments size=\"+mSegmentList.size());\n        while (iterator.hasNext()) {\n            Map.Entry<Long, Long> item = (Map.Entry<Long, Long>)iterator.next();\n            long start = item.getKey();\n            long end = item.getValue();\n            mVideoRangeMap.put(start, new VideoRange(start, end));\n        }\n        printVideoRange();\n    }\n\n    private synchronized VideoRange getVideoRequestRange(long curSeekPosition) {\n        long rangeStart = 0;\n        long rangeEnd = Long.MAX_VALUE;\n        printVideoRange();\n        Iterator iterator = mVideoRangeMap.entrySet().iterator();\n        while(iterator.hasNext()) {\n            Map.Entry<Long, VideoRange> item = (Map.Entry<Long, VideoRange>)iterator.next();\n            VideoRange range = item.getValue();\n            if (range.start > curSeekPosition) {\n                rangeEnd = range.start;\n                break;\n            }\n            if (range.start <= curSeekPosition && range.end >= curSeekPosition) {\n                rangeStart = range.end;\n                continue;\n            }\n            if (curSeekPosition > range.end + BUFFER_SIZE) {\n                rangeStart = curSeekPosition;\n                continue;\n            } else {\n                rangeStart = range.end;\n                continue;\n            }\n        }\n        VideoRange range = new VideoRange(rangeStart, rangeEnd);\n        return range;\n    }\n\n    @Override\n    public void startDownload(IDownloadTaskListener listener) {\n        mDownloadTaskListener = listener;\n        if (listener != null) {\n            listener.onTaskStart(mInfo.getUrl());\n        }\n        mIsPlaying = false;\n        seekToDownload(0L, listener);\n    }\n\n    @Override\n    public void resumeDownload() {\n        LogUtils.i(\"BaseVideoDownloadTask resumeDownload current position=\"+mCurrentCachedSize);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(mCurrentCachedSize, mDownloadTaskListener);\n    }\n\n    @Override\n    public void seekToDownload(float seekPercent) {\n        seekToDownload(seekPercent, mDownloadTaskListener);\n    }\n\n    @Override\n    public void seekToDownload(long curPosition, long totalDuration) {\n        pauseDownload();\n        long curSeekPosition = (long)(curPosition * 1.0f / totalDuration * mTotalLength);\n        LogUtils.i(\"BaseVideoDownloadTask seekToDownload seekToDownload=\"+curSeekPosition);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(curSeekPosition, mDownloadTaskListener);\n    }\n\n    @Override\n    public void seekToDownload(float seekPercent, IDownloadTaskListener callback) {\n        pauseDownload();\n        long curSeekPosition = (long)(seekPercent * 1.0f / 100 * mTotalLength);\n        LogUtils.i(\"BaseVideoDownloadTask seekToDownload seekToDownload=\"+curSeekPosition);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(curSeekPosition, callback);\n    }\n\n    //Just for M3U8VideoDownloadTask.\n    @Override\n    public void seekToDownload(int curDownloadTs, IDownloadTaskListener callback) {\n\n    }\n\n    @Override\n    public void seekToDownload(long curSeekPosition, IDownloadTaskListener listener) {\n        if (mInfo.getIsCompleted()) {\n            LogUtils.i(\"BaseVideoDownloadTask local file.\");\n            notifyVideoReady();\n            notifyCacheProgress();\n            return;\n        }\n        startTimerTask();\n        mDownloadExecutor = new ThreadPoolExecutor(\n                THREAD_COUNT, THREAD_COUNT, 0L, TimeUnit.MILLISECONDS,\n                new LinkedBlockingQueue<>(),\n                Executors.defaultThreadFactory(),\n                new ThreadPoolExecutor.DiscardOldestPolicy());\n        mDownloadExecutor.execute(new Runnable() {\n            @Override\n            public void run() {\n                mCurDownloadRange = getVideoRequestRange(curSeekPosition);\n                LogUtils.i(\"seekToDownload ### mCurDownloadRange=\"+mCurDownloadRange);\n                if (mTotalLength == -1L) {\n                    mTotalLength = getContentLength(mFinalUrl);\n                    LogUtils.i(\"file length = \" + mTotalLength);\n                    if (mTotalLength <= 0) {\n                        LogUtils.w(\"BaseVideoDownloadTask file length cannot be fetched.\");\n                        notifyFailed(new VideoCacheException(DownloadExceptionUtils.FILE_LENGTH_FETCHED_ERROR_STRING));\n                        return;\n                    }\n                    mInfo.setTotalLength(mTotalLength);\n                }\n                File videoFile;\n                try {\n                    videoFile = new File(mSaveDir, mSaveName + VIDEO_SUFFIX);\n                    if (!videoFile.exists()) {\n                        videoFile.createNewFile();\n                    }\n                } catch (Exception e) {\n                    LogUtils.w(\"BaseDownloadTask createNewFile failed, exception=\"+e.getMessage());\n                    return;\n                }\n\n                if (videoFile != null && videoFile.exists() &&\n                        videoFile.length() > BUFFER_SIZE) {\n                    notifyVideoReady();\n                }\n\n                if (mCurDownloadRange.start == Long.MIN_VALUE) {\n                    mCurDownloadRange.start = 0;\n                }\n                if (mCurDownloadRange.end == Long.MAX_VALUE) {\n                    mCurDownloadRange.end = mTotalLength;\n                }\n\n                InputStream inputStream = null;\n                RandomAccessFile randomAccessFile = null;\n                try {\n                    LogUtils.i(\"seekToDownload start request video range:\" + mCurDownloadRange);\n                    LogUtils.i(\"begin request\");\n                    long rangeEnd = mCurDownloadRange.end;\n                    long rangeStart = mCurDownloadRange.start;\n                    mCurrentCachedSize = rangeStart;\n\n                    inputStream = getResponseBody(mFinalUrl, rangeStart, rangeEnd);\n                    byte[] buf = new byte[BUFFER_SIZE];\n\n                    LogUtils.i(\"begin response\");\n\n                    //Read http stream body.\n\n                    randomAccessFile = new RandomAccessFile(videoFile.getAbsolutePath(), \"rw\");\n                    randomAccessFile.seek(rangeStart);\n                    int readLength = 0;\n                    while ((readLength = inputStream.read(buf)) != -1) {\n                        if (mCurrentCachedSize >= rangeEnd) {\n                            mCurrentCachedSize = rangeEnd;\n                        }\n                        if (mCurrentCachedSize + readLength > rangeEnd) {\n                            randomAccessFile.write(buf, 0, (int)(rangeEnd - mCurrentCachedSize));\n                            mCurrentCachedSize = rangeEnd;\n                        } else {\n                            randomAccessFile.write(buf, 0, readLength);\n                            mCurrentCachedSize += readLength;\n                        }\n                        notifyCacheProgress();\n                        if (mCurrentCachedSize >= BUFFER_SIZE + rangeStart) {\n                            notifyVideoReady();\n                        }\n                        if (mCurrentCachedSize >= rangeEnd) {\n                            LogUtils.w(\"BaseVideoDownloadTask innerThread segment download finished.\");\n                            notifyNextVideoSegment(rangeEnd);\n                        }\n                    }\n                } catch (IOException e) {\n                    LogUtils.w( \"BaseVideo Download file failed, exception: \" + e);\n\n                    StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n\n                    //InterruptedIOException is just interrupted by external operation.\n                    if (e instanceof InterruptedIOException) {\n                        return;\n                    }\n\n                    notifyFailed(e);\n                    return;\n                } finally {\n                    try {\n                        if (inputStream != null) {\n                            inputStream.close();\n                        }\n                        if (randomAccessFile != null) {\n                            randomAccessFile.close();\n                        }\n                    } catch (IOException e) {\n                        LogUtils.w( \"Close stream failed, exception: \" + e.getMessage());\n                    }\n                }\n\n            }\n        });\n\n        //mCurDownloadRange download finished. Please download next range.\n        //1.pauseDownload;\n        //2.seekToDownload next range;\n    }\n\n    private void notifyFailed(Throwable e) {\n        if (mDownloadTaskListener != null){\n            mDownloadTaskListener.onTaskFailed(e);\n        }\n    }\n\n    @Override\n    public void pauseDownload() {\n        if (mDownloadExecutor != null && !mDownloadExecutor.isShutdown()) {\n            mDownloadExecutor.shutdownNow();\n            mShouldSuspendDownloadTask = true;\n            notifyOnTaskPaused();\n        }\n        updateProxyCacheInfo();\n        writeProxyCacheInfo();\n        StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n    }\n\n    @Override\n    public void stopDownload() {\n        if (mDownloadExecutor != null && !mDownloadExecutor.isShutdown()) {\n            mDownloadExecutor.shutdownNow();\n            mShouldSuspendDownloadTask = true;\n            notifyOnTaskPaused();\n        }\n        updateProxyCacheInfo();\n        writeProxyCacheInfo();\n        StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n    }\n\n    private synchronized void updateProxyCacheInfo() {\n        LogUtils.i( \"BaseVideoDownloadTask updateProxyCacheInfo\");\n        if (!isCompleted()) {\n            if (mCurrentCachedSize > mTotalLength)\n                mCurDownloadRange.end = mTotalLength;\n            else\n                mCurDownloadRange.end = mCurrentCachedSize;\n            mergeVideoRange();\n            mInfo.setCachedLength(mCurDownloadRange.end);\n            mInfo.setIsCompleted(isCompleted());\n        } else {\n            mInfo.setIsCompleted(true);\n        }\n        if (mInfo.getIsCompleted()) {\n            notifyCacheFinished();\n        }\n    }\n\n    private void writeProxyCacheInfo() {\n        if (mType == OPERATE_TYPE.WRITED) {\n            return;\n        }\n        LocalProxyThreadUtils.submitRunnableTask(new Runnable() {\n            @Override\n            public void run() {\n                mInfo.setPort(mConfig.getPort());\n                LogUtils.i(\"writeProxyCacheInfo : \" + mInfo);\n                LocalProxyUtils.writeProxyCacheInfo(mInfo, mSaveDir);\n            }\n        });\n        if (mType == OPERATE_TYPE.DEFAULT && mInfo.getIsCompleted()) {\n            mType = OPERATE_TYPE.WRITED;\n        }\n    }\n\n    private synchronized void mergeVideoRange() {\n        //merge  mCurDownloadRange in  mVideoRangeMap.\n        if (mVideoRangeMap.size() < 1) {\n            LogUtils.i(\"mergeVideoRange mCurDownloadRange=\"+mCurDownloadRange);\n            if (mCurDownloadRange.start != Long.MIN_VALUE &&\n                    mCurDownloadRange.end != Long.MAX_VALUE &&\n                    mCurDownloadRange.start < mCurDownloadRange.end) {\n                mVideoRangeMap.put(mCurDownloadRange.start, mCurDownloadRange);\n            } else {\n                LogUtils.i( \"mergeVideoRange Cannot merge video range.\");\n            }\n        } else if (!mVideoRangeMap.containsValue(mCurDownloadRange)) {\n            LogUtils.i(\"mergeVideoRange rangeLength>1, mCurDownloadRange=\"+mCurDownloadRange);\n\n            if (mCurDownloadRange.start == Long.MIN_VALUE\n                    || mCurDownloadRange.end == Long.MAX_VALUE\n                    || mCurDownloadRange.start >= mCurDownloadRange.end\n                    || mCurrentCachedSize <= mCurDownloadRange.start) {\n                return;\n            }\n\n            //1.Convert mCurDownloadRange into FinalRange.\n            VideoRange finalRange = new VideoRange(Long.MIN_VALUE, Long.MAX_VALUE);\n            Iterator iterator = mVideoRangeMap.entrySet().iterator();\n            while (iterator.hasNext()) {\n                Map.Entry<Long, VideoRange> item = (Map.Entry<Long, VideoRange>) iterator.next();\n                VideoRange range = item.getValue();\n                LogUtils.i(\"mergeVideoRange  item range=\"+range);\n                if (range.start > mCurDownloadRange.end) {\n                    finalRange.end = mCurDownloadRange.end;\n                    break;\n                }\n                if (range.start <= mCurDownloadRange.end && range.end >= mCurDownloadRange.end) {\n                    finalRange.end = range.end;\n                    break;\n                }\n                if (range.end >= mCurDownloadRange.start && range.start <= mCurDownloadRange.start) {\n                    finalRange.start = range.start;\n                    continue;\n                }\n                if (range.end < mCurDownloadRange.start) {\n                    finalRange.start = mCurDownloadRange.start;\n                    continue;\n                }\n            }\n\n            //2.Generate FinalRange.\n            if (finalRange.start == Long.MIN_VALUE) {\n                finalRange.start = mCurDownloadRange.start;\n            }\n            if (finalRange.end == Long.MAX_VALUE) {\n                finalRange.end = mCurDownloadRange.end;\n            }\n            LogUtils.i(\"finalRange = \" + finalRange);\n            //3.Put FinalRange into mVideoRangeMap container.\n            mVideoRangeMap.put(finalRange.start, finalRange);\n            //4.Remove redundancy range from mVideoRangeMap.\n            iterator = mVideoRangeMap.entrySet().iterator();\n            LinkedHashMap<Long, VideoRange> tempVideoRangeMap = new LinkedHashMap<>();\n            while(iterator.hasNext()) {\n                Map.Entry<Long, VideoRange> item = (Map.Entry<Long, VideoRange>) iterator.next();\n                VideoRange range = item.getValue();\n                if (!containRange(finalRange, range)) {\n                    tempVideoRangeMap.put(range.start, range);\n                }\n            }\n            mVideoRangeMap.clear();\n            mVideoRangeMap.putAll(tempVideoRangeMap);\n        }\n        LinkedHashMap<Long, Long> tempSegmentList = new LinkedHashMap<>();\n        Iterator iterator = mVideoRangeMap.entrySet().iterator();\n        while (iterator.hasNext()) {\n            Map.Entry<Long, VideoRange> item = (Map.Entry<Long, VideoRange>) iterator.next();\n            VideoRange range = item.getValue();\n            tempSegmentList.put(range.start, range.end);\n        }\n        mSegmentList.clear();\n        mSegmentList.putAll(tempSegmentList);\n        mInfo.setSegmentList(mSegmentList);\n    }\n\n    //5.Determine video cache is complete?\n    private synchronized boolean isCompleted() {\n        if (mVideoRangeMap.size() != 1) {\n            return false;\n        }\n        //The key is 0L, not 0; remember it.\n        VideoRange range = mVideoRangeMap.get(0L);\n        if (range != null && range.end == mTotalLength) {\n            return true;\n        }\n        return false;\n    }\n\n    private boolean containRange(VideoRange range1, VideoRange range2) {\n        return range1.start < range2.start && range1.end >= range2.end;\n    }\n\n    private synchronized void printVideoRange() {\n        Iterator iterator = mVideoRangeMap.entrySet().iterator();\n        LogUtils.i(\"printVideoRange size=\"+mVideoRangeMap.size());\n        while (iterator.hasNext()) {\n            Map.Entry<Long, VideoRange> item = (Map.Entry<Long, VideoRange>) iterator.next();\n            VideoRange range = item.getValue();\n            LogUtils.i( \"printVideoRange range=\"+range);\n        }\n    }\n\n    private synchronized void notifyVideoReady() {\n        if (mDownloadTaskListener != null && !mIsPlaying) {\n            String proxyUrl = String.format(Locale.US, \"http://%s:%d/%s/%s\", mConfig.getHost(), mConfig.getPort(), mSaveName, mSaveName + VIDEO_SUFFIX);\n            mDownloadTaskListener.onLocalProxyReady(proxyUrl);//Uri.fromFile(mM3u8Help.getFile()).toString());\n            mIsPlaying = true;\n        }\n    }\n\n    private void notifyCacheProgress() {\n        if (mDownloadTaskListener != null) {\n            if (mInfo.getIsCompleted()) {\n                if (!LocalProxyUtils.isFloatEqual(100.0f, mPercent)) {\n                    mDownloadTaskListener.onTaskProgress(100,\n                            mTotalLength, null);\n                }\n                mPercent = 100.0f;\n                notifyCacheFinished();\n            } else {\n                mInfo.setCachedLength(mCurrentCachedSize);\n                float percent = mCurrentCachedSize * 1.0f * 100 / mTotalLength;\n                if (!LocalProxyUtils.isFloatEqual(percent, mPercent)) {\n                    mDownloadTaskListener.onTaskProgress(percent,\n                            mCurrentCachedSize, null);\n                    mPercent = percent;\n                }\n            }\n        }\n    }\n\n    //1.current segment has been downloaded.\n    //2.start to download next video's segment.\n    private void notifyNextVideoSegment(long rangeStart) {\n        pauseDownload();\n        if (rangeStart < mTotalLength) {\n            seekToDownload(rangeStart, mDownloadTaskListener);\n        }\n    }\n\n    private void notifyCacheFinished() {\n        if (mDownloadTaskListener != null) {\n            writeProxyCacheInfo();\n            mDownloadTaskListener.onTaskFinished(mTotalLength);\n            StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n        }\n    }\n\n    private InputStream getResponseBody(String url, long start, long end) throws IOException {\n        HttpURLConnection connection = openConnection(url);\n        connection.setRequestProperty(\"Range\",\n                \"bytes=\" + start + \"-\" +\n                        end);\n        return connection.getInputStream();\n    }\n\n    private long getContentLength(String videoUrl) {\n        long length = 0;\n        HttpURLConnection connection = null;\n        try {\n            connection = openConnection(videoUrl);\n            length = connection.getContentLength();\n        } catch (Exception e) {\n            LogUtils.w( \"BaseDownloadTask failed, exception=\"+e.getMessage());\n        } finally {\n            if (connection != null) {\n                connection.disconnect();\n                connection = null;\n            }\n        }\n        return length;\n    }\n\n    private HttpURLConnection openConnection(String videoUrl)\n            throws IOException {\n        HttpURLConnection connection;\n        URL url = new URL(videoUrl);\n        connection = (HttpURLConnection)url.openConnection();\n        if (mConfig.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n            HttpUtils.trustAllCert((HttpsURLConnection)(connection));\n        }\n        connection.setConnectTimeout(mConfig.getReadTimeOut());\n        connection.setReadTimeout(mConfig.getConnTimeOut());\n        if (mHeaders != null) {\n            for (Map.Entry<String, String> item : mHeaders.entrySet()) {\n                connection.setRequestProperty(item.getKey(), item.getValue());\n            }\n        }\n        return connection;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/download/M3U8VideoDownloadTask.java",
    "content": "package com.media.cache.download;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.StorageManager;\nimport com.media.cache.hls.M3U8Constants;\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.hls.M3U8;\nimport com.media.cache.hls.M3U8Ts;\nimport com.media.cache.listener.IDownloadTaskListener;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyThreadUtils;\nimport com.media.cache.utils.LocalProxyUtils;\nimport com.media.cache.utils.StorageUtils;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InterruptedIOException;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.SocketTimeoutException;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\nimport javax.net.ssl.HttpsURLConnection;\n\npublic class M3U8VideoDownloadTask extends VideoDownloadTask {\n\n    private static final String TS_PREFIX = \"seg_\";\n    private final M3U8 mM3U8;\n    private List<M3U8Ts> mTsList;\n    private volatile int mCurTs = 0;\n    private int mTotalTs;\n    private long mDuration;\n    private long mTotalSize;\n    private final Object mFileLock = new Object();\n\n    public M3U8VideoDownloadTask(LocalProxyConfig config,\n                                 VideoCacheInfo info, M3U8 m3u8,\n                                 HashMap<String, String> headers) {\n        super(config, info, headers);\n        this.mM3U8 = m3u8;\n        this.mTsList = m3u8.getTsList();\n        this.mTotalTs = mTsList.size();\n        this.mCurTs = info.getCachedTs();\n        this.mTotalSize = info.getTotalLength();\n        this.mPercent = info.getPercent();\n        this.mDuration = m3u8.getDuration();\n        if (mDuration == 0) {\n            mDuration = 1;\n        }\n        LogUtils.w(\"jeffmony port=\"+config.getPort());\n        info.setTotalTs(mTotalTs);\n        info.setCachedTs(mCurTs);\n    }\n\n    @Override\n    public void startDownload(IDownloadTaskListener listener) {\n        mDownloadTaskListener = listener;\n        if (listener != null) {\n            listener.onTaskStart(mInfo.getUrl());\n        }\n        mIsPlaying = false;\n        LogUtils.i(\"startDownload=\"+mCurTs);\n        // Download hls resource from 0 index.\n        seekToDownload(mCurTs, listener);\n    }\n\n    @Override\n    public void resumeDownload() {\n        LogUtils.i(\"M3U8VideoDownloadTask resumeDownload, curTs=\"+mCurTs);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(mCurTs, mDownloadTaskListener);\n\n    }\n\n    @Override\n    public void seekToDownload(float seekPercent) {\n        seekToDownload(seekPercent, mDownloadTaskListener);\n    }\n\n    @Override\n    public void seekToDownload(long curPosition, long totalDuration) {\n        pauseDownload();\n        //Download hls resource from the seeking position.\n        LogUtils.i(\"seekToDownload curPosition=\"+curPosition +\", totalDuration=\"+totalDuration+\", \"+mDuration);\n        if (mDuration != totalDuration && totalDuration != 0) {\n            mDuration = totalDuration;\n        }\n        int curDownloadTs = mM3U8.getTsIndex(curPosition / 1000);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(curDownloadTs, mDownloadTaskListener);\n    }\n\n    @Override\n    public void seekToDownload(float seekPercent, IDownloadTaskListener listener) {\n        pauseDownload();\n        if (seekPercent < 0) {\n            seekPercent = 0f;\n        }\n        //Download hls resource from the seeking position.\n        long curPosition = (long)(seekPercent * 1.0f / 100 * mDuration);\n        LogUtils.i(\"seekToDownload curPosition=\"+curPosition);\n        int curDownloadTs = mM3U8.getTsIndex(curPosition);\n        mShouldSuspendDownloadTask = false;\n        seekToDownload(curDownloadTs, listener);\n    }\n\n    @Override\n    public void seekToDownload(int curDownloadTs, IDownloadTaskListener listener) {\n        if (mInfo.getIsCompleted()) {\n            LogUtils.i(\"M3U8VideoDownloadTask local file.\");\n            notifyVideoCompleted();\n            return;\n        }\n        startTimerTask();\n        mCurTs = curDownloadTs;\n        LogUtils.i(\"seekToDownload curDownloadTs = \" + curDownloadTs);\n        mDownloadExecutor = new ThreadPoolExecutor(\n                THREAD_COUNT, THREAD_COUNT, 0L, TimeUnit.MILLISECONDS,\n                new LinkedBlockingQueue<>(),\n                Executors.defaultThreadFactory(),\n                new ThreadPoolExecutor.DiscardOldestPolicy());\n        for (int index = curDownloadTs; index < mTotalTs; index++) {\n            if (mDownloadExecutor.isShutdown()) {\n                break;\n            }\n            M3U8Ts ts = mTsList.get(index);\n            String tsName = TS_PREFIX + index + \".ts\";\n            File tsFile = new File(mSaveDir, tsName);\n            mDownloadExecutor.execute(new Runnable() {\n                @Override\n                public void run() {\n                    try {\n                        if (isM3U8FileExisted()) {\n                            if (mConfig.getPort() != mInfo.getPort()) {\n                                createM3U8File();\n                            }\n                            notifyVideoReady();\n                        } else {\n                            createM3U8File();\n                            notifyVideoReady();\n                        }\n                        downloadTsTask(ts, tsFile, tsName);\n                    } catch (Exception e) {\n                        LogUtils.w( \"M3U8TsDownloadThread download failed, exception=\"+e);\n                        notifyFailed(e);\n                    }\n                }\n            });\n        }\n\n        notifyCacheFinished(mCurrentCachedSize);\n    }\n\n    private void notifyVideoCompleted() {\n        LocalProxyThreadUtils.submitRunnableTask(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    if (mConfig.getPort() != mInfo.getPort()) {\n                        createM3U8File();\n                    }\n                    notifyVideoReady();\n                    notifyCacheProgress();\n                    notifyCacheFinished(mTotalSize);\n                } catch (Exception e) {\n                    LogUtils.w(\"M3U8TsDownloadThread createM3U8File failed.\");\n                }\n            }\n        });\n    }\n\n    private void downloadTsTask(M3U8Ts ts, File tsFile, String tsName) throws Exception {\n        if (!tsFile.exists()) {\n            //ts is network resource, download ts file then rename it to local file.\n            downloadFile(ts.getUrl(), tsFile);\n        }\n\n        if (tsFile.exists()) {\n            //rename network ts name to local file name.\n            ts.setName(tsName);\n            ts.setTsSize(tsFile.length());\n            mCurTs++;\n            notifyCacheProgress();\n        }\n    }\n\n    //Just for BaseVideoDownloadTask.\n    @Override\n    public void seekToDownload(long curLength, IDownloadTaskListener callback) {\n\n    }\n\n    @Override\n    public void pauseDownload() {\n        if (mDownloadExecutor != null && !mDownloadExecutor.isShutdown()) {\n            mDownloadExecutor.shutdownNow();\n            mShouldSuspendDownloadTask = true;\n            notifyOnTaskPaused();\n        }\n        updateProxyCacheInfo();\n    }\n\n    @Override\n    public void stopDownload() {\n        if (mDownloadExecutor != null && !mDownloadExecutor.isShutdown()) {\n            mDownloadExecutor.shutdownNow();\n            mShouldSuspendDownloadTask = true;\n            notifyOnTaskPaused();\n        }\n        updateProxyCacheInfo();\n        StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n    }\n\n    private boolean isM3U8FileExisted() {\n        synchronized (mFileLock) {\n            return new File(mSaveDir, \"proxy.m3u8\").exists();\n        }\n    }\n\n    private void notifyVideoReady() {\n        if (mDownloadTaskListener != null && !mIsPlaying) {\n            LogUtils.i( \"M3U8VideoDownloadTask notifyVideoReady\");\n            String proxyUrl = String.format(Locale.US, \"http://%s:%d/%s/%s\", mConfig.getHost(), mConfig.getPort(), mSaveName, \"proxy.m3u8\");\n            mDownloadTaskListener.onLocalProxyReady(proxyUrl);//Uri.fromFile(mM3u8Help.getFile()).toString());\n            mIsPlaying = true;\n        }\n    }\n\n    private void notifyFailed(Exception e) {\n        StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n        //InterruptedIOException is just interrupted by external operation.\n        if (e instanceof InterruptedException || e instanceof InterruptedIOException) {\n            if (e instanceof SocketTimeoutException) {\n                LogUtils.w(\"M3U8VideoDownloadTask notifyFailed: \" + e);\n                resumeDownload();\n                return;\n            }\n            pauseDownload();\n            writeProxyCacheInfo();\n            return;\n        } else if (e instanceof MalformedURLException){\n            String parsedString = \"no protocol: \";\n            if (e.toString().contains(parsedString)) {\n                String fileName = e.toString().substring(e.toString().indexOf(parsedString) + parsedString.length());\n                LogUtils.w(fileName + \" not existed.\");\n            }\n            return;\n        }\n        if (mDownloadTaskListener != null) {\n            mDownloadTaskListener.onTaskFailed(e);\n        }\n    }\n\n    private void updateProxyCacheInfo() {\n        boolean isCompleted = true;\n        for (M3U8Ts ts : mTsList) {\n            File tsFile = new File(mSaveDir, ts.getIndexName());\n            if (!tsFile.exists()) {\n                isCompleted = false;\n                break;\n            }\n        }\n        mInfo.setIsCompleted(isCompleted);\n        if (isCompleted) {\n            writeProxyCacheInfo();\n        }\n    }\n\n    private void writeProxyCacheInfo() {\n        if (mType == OPERATE_TYPE.WRITED) {\n            return;\n        }\n        LocalProxyThreadUtils.submitRunnableTask(new Runnable() {\n            @Override\n            public void run() {\n                mInfo.setPort(mConfig.getPort());\n                LogUtils.i(\"writeProxyCacheInfo : \" + mInfo);\n                LocalProxyUtils.writeProxyCacheInfo(mInfo, mSaveDir);\n            }\n        });\n        if (mType == OPERATE_TYPE.DEFAULT && mInfo.getIsCompleted()) {\n            mType = OPERATE_TYPE.WRITED;\n        }\n    }\n\n    private void notifyCacheFinished(long size) {\n        if (mDownloadTaskListener != null) {\n            updateProxyCacheInfo();\n            if (mInfo.getIsCompleted()) {\n                mDownloadTaskListener.onTaskFinished(size);\n                StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n            }\n        }\n    }\n\n    private void notifyCacheProgress() {\n        if (mDownloadTaskListener != null) {\n            mCurrentCachedSize = 0;\n            for (M3U8Ts ts : mTsList) {\n                mCurrentCachedSize += ts.getTsSize();\n            }\n            if (mCurrentCachedSize == 0) {\n                mCurrentCachedSize = StorageUtils.countTotalSize(mSaveDir);\n            }\n            if (mInfo.getIsCompleted()) {\n                mCurTs = mTotalTs;\n                StorageManager.getInstance().checkCacheFile(mSaveDir, mConfig.getCacheSize());\n                if (!LocalProxyUtils.isFloatEqual(100.0f, mPercent)) {\n                    mDownloadTaskListener.onTaskProgress(100.0f,\n                            mCurrentCachedSize, mM3U8);\n                }\n                mPercent = 100.0f;\n                mTotalSize = mCurrentCachedSize;\n                mDownloadTaskListener.onTaskFinished(mTotalSize);\n                return;\n            }\n            if (mCurTs >= mTotalTs - 1) {\n                mCurTs = mTotalTs;\n            }\n            mInfo.setCachedTs(mCurTs);\n            mM3U8.setCurTsIndex(mCurTs);\n            float percent = mCurTs * 1.0f * 100 / mTotalTs;\n            if (!LocalProxyUtils.isFloatEqual(percent, mPercent)) {\n                mDownloadTaskListener.onTaskProgress(percent,\n                        mCurrentCachedSize, mM3U8);\n                mPercent = percent;\n                mInfo.setPercent(percent);\n                mInfo.setCachedLength(mCurrentCachedSize);\n            }\n            boolean isCompleted = true;\n            for (M3U8Ts ts : mTsList) {\n                File tsFile = new File(mSaveDir, ts.getIndexName());\n                if (!tsFile.exists()) {\n                    isCompleted = false;\n                    break;\n                }\n            }\n            mInfo.setIsCompleted(isCompleted);\n            if (isCompleted) {\n                mInfo.setTotalLength(mCurrentCachedSize);\n                mTotalSize = mCurrentCachedSize;\n                mDownloadTaskListener.onTaskFinished(mTotalSize);\n                writeProxyCacheInfo();\n            }\n        }\n    }\n\n    private static final int REDIRECTED_COUNT = 3;\n\n    public void downloadFile(String url, File file) throws Exception {\n        HttpURLConnection connection = null;\n        InputStream inputStream = null;\n        try {\n            connection = openConnection(url);\n            int responseCode = connection.getResponseCode();\n            if (responseCode == HttpUtils.RESPONSE_OK) {\n                inputStream = connection.getInputStream();\n                saveFile(inputStream, file);\n            }\n        }catch (Exception e) {\n            throw e;\n        }finally {\n            if (connection != null)\n                connection.disconnect();\n            LocalProxyUtils.close(inputStream);\n        }\n\n    }\n\n    private HttpURLConnection openConnection(String videoUrl)\n            throws Exception {\n        HttpURLConnection connection;\n        boolean redirected;\n        int redirectedCount = 0;\n        do {\n            URL url = new URL(videoUrl);\n            connection = (HttpURLConnection)url.openConnection();\n\n            if (mConfig.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n                HttpUtils.trustAllCert((HttpsURLConnection)(connection));\n            }\n            connection.setConnectTimeout(mConfig.getConnTimeOut());\n            connection.setReadTimeout(mConfig.getReadTimeOut());\n            if (mHeaders != null) {\n                for (Map.Entry<String, String> item : mHeaders.entrySet()) {\n                    connection.setRequestProperty(item.getKey(), item.getValue());\n                }\n            }\n            int code = connection.getResponseCode();\n            redirected = code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_MOVED_TEMP ||\n                    code == HttpURLConnection.HTTP_SEE_OTHER;\n            if (redirected) {\n                redirectedCount++;\n                connection.disconnect();\n            }\n            if (redirectedCount > REDIRECTED_COUNT) {\n                throw new Exception(\"Too many redirects: \" +\n                        redirectedCount);\n            }\n        } while (redirected);\n        return connection;\n    }\n\n    private void saveFile(InputStream inputStream, File file) {\n        FileOutputStream fos = null;\n        try {\n            fos = new FileOutputStream(file);\n            int len = 0;\n            byte[] buf = new byte[BUFFER_SIZE];\n            while ((len = inputStream.read(buf)) != -1) {\n                fos.write(buf, 0, len);\n            }\n        } catch (IOException e) {\n            LogUtils.w(file.getAbsolutePath() + \" saveFile failed, exception=\"+e);\n            if (file.exists()) {\n                file.delete();\n            }\n        } finally {\n            LocalProxyUtils.close(inputStream);\n            LocalProxyUtils.close(fos);\n        }\n    }\n\n    private void createM3U8File() throws IOException {\n        synchronized (mFileLock) {\n            File tempM3U8File = new File(mSaveDir, \"temp.m3u8\");\n            if (tempM3U8File.exists()) {\n                tempM3U8File.delete();\n            }\n\n            BufferedWriter bfw = new BufferedWriter(new FileWriter(tempM3U8File, false));\n            bfw.write(M3U8Constants.PLAYLIST_HEADER + \"\\n\");\n            bfw.write(M3U8Constants.TAG_VERSION + \":\" + mM3U8.getVersion() + \"\\n\");\n            bfw.write(M3U8Constants.TAG_MEDIA_SEQUENCE + \":\" + mM3U8.getSequence()+\"\\n\");\n\n            bfw.write(M3U8Constants.TAG_TARGET_DURATION + \":\" + mM3U8.getTargetDuration() + \"\\n\");\n\n            for (M3U8Ts m3u8Ts : mTsList) {\n                if (m3u8Ts.hasKey()) {\n                    if (m3u8Ts.getMethod() != null) {\n                        String key = \"METHOD=\" + m3u8Ts.getMethod();\n                        if (m3u8Ts.getKeyUri() != null) {\n                            File keyFile = new File(mSaveDir, m3u8Ts.getLocalKeyUri());\n                            if (!m3u8Ts.isMessyKey() && keyFile.exists()) {\n                                key += \",URI=\\\"\" + m3u8Ts.getLocalKeyUri() + \"\\\"\";\n                            } else {\n                                key += \",URI=\\\"\" + m3u8Ts.getKeyUri() + \"\\\"\";\n                            }\n                        }\n                        if (m3u8Ts.getKeyIV() != null) {\n                            key += \",IV=\" + m3u8Ts.getKeyIV();\n                        }\n                        bfw.write(M3U8Constants.TAG_KEY + \":\" + key + \"\\n\");\n                    }\n                }\n                if (m3u8Ts.hasDiscontinuity()) {\n                    bfw.write(M3U8Constants.TAG_DISCONTINUITY+\"\\n\");\n                }\n                bfw.write(M3U8Constants.TAG_MEDIA_DURATION + \":\" + m3u8Ts.getDuration()+\",\\n\");\n                bfw.write(m3u8Ts.getProxyUrl(mConfig.getHost(), mConfig.getPort(), mSaveName));\n                bfw.newLine();\n            }\n            bfw.write(M3U8Constants.TAG_ENDLIST);\n            bfw.flush();\n            bfw.close();\n\n            File localM3U8File = new File(mSaveDir, \"proxy.m3u8\");\n            if (localM3U8File.exists()) {\n                localM3U8File.delete();\n            }\n            tempM3U8File.renameTo(localM3U8File);\n        }\n    }\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/download/VideoDownloadTask.java",
    "content": "package com.media.cache.download;\n\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.listener.IDownloadTaskListener;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport java.util.concurrent.ThreadPoolExecutor;\n\npublic abstract class VideoDownloadTask {\n\n    protected static final int THREAD_COUNT = 3;\n    protected static final int BUFFER_SIZE = LocalProxyUtils.DEFAULT_BUFFER_SIZE;\n\n    protected ThreadPoolExecutor mDownloadExecutor;\n    protected IDownloadTaskListener mDownloadTaskListener;\n    protected volatile boolean mShouldSuspendDownloadTask = false;\n    protected volatile boolean mIsPlaying = false;\n    protected final LocalProxyConfig mConfig;\n    protected final VideoCacheInfo mInfo;\n    protected final String mFinalUrl;\n    protected final HashMap<String, String> mHeaders;\n    protected File mSaveDir;\n    protected String mSaveName;\n    protected Timer mTimer;\n    protected long mOldCachedSize = 0L;\n    protected long mCurrentCachedSize = 0L;\n    protected float mPercent = 0.0f;\n    protected float mSpeed = 0.0f;\n\n    protected volatile OPERATE_TYPE mType = OPERATE_TYPE.DEFAULT;\n    protected enum OPERATE_TYPE {\n        DEFAULT,\n        WRITED,\n    }\n\n    protected VideoDownloadTask(LocalProxyConfig config,\n                                VideoCacheInfo info,\n                                HashMap<String, String> headers) {\n        mConfig = config;\n        mInfo = info;\n        mHeaders = headers;\n        mFinalUrl = info.getFinalUrl();\n        mSaveName = LocalProxyUtils.computeMD5(info.getUrl());\n        mSaveDir = new File(mConfig.getCacheRoot(), mSaveName);\n        if (!mSaveDir.exists()) {\n            mSaveDir.mkdir();\n        }\n        info.setSaveDir(mSaveDir.getAbsolutePath());\n    }\n\n    protected void startTimerTask() {\n        if (mTimer == null) {\n            mTimer = new Timer();\n            TimerTask task = new TimerTask() {\n                @Override\n                public void run() {\n                    if (mOldCachedSize <= mCurrentCachedSize) {\n                        float speed = (mCurrentCachedSize - mOldCachedSize) * 1.0f;\n                        mDownloadTaskListener.onTaskSpeedChanged(speed);\n                        mOldCachedSize = mCurrentCachedSize;\n                        mSpeed = speed;\n                    }\n                }\n            };\n            mTimer.schedule(task, 0, LocalProxyUtils.UPDATE_INTERVAL);\n        }\n    }\n\n    protected void cancelTimer() {\n        if (mTimer != null) {\n            mTimer.cancel();\n            mTimer = null;\n        }\n    }\n\n    protected void notifyOnTaskPaused() {\n        if (mDownloadTaskListener != null) {\n            mDownloadTaskListener.onTaskPaused();\n            cancelTimer();\n        }\n    }\n\n    public abstract void startDownload(IDownloadTaskListener listener);\n\n    public abstract void resumeDownload();\n\n    public abstract void seekToDownload(float seekPercent);\n\n    public abstract void seekToDownload(long curPosition, long totalDuration);\n\n    public abstract void seekToDownload(float seekPercent, IDownloadTaskListener callback);\n\n    public abstract void seekToDownload(int curDownloadTs, IDownloadTaskListener callback);\n\n    public abstract void seekToDownload(long curLength, IDownloadTaskListener callback);\n\n    public abstract void pauseDownload();\n\n    public abstract void stopDownload();\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/hls/M3U8.java",
    "content": "package com.media.cache.hls;\n\nimport com.android.baselib.utils.LogUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class M3U8 {\n\n    private String mUrl;\n    private String mBaseUrl;\n    private String mHostUrl;\n    private List<M3U8Ts> mTsList;\n    private float mTargetDuration;\n    private int mSequence = 0;\n    private int mVersion = 3;\n    private boolean mHasEndList;\n    private int mCurTsIndex = 0;\n\n    public M3U8(String url, String baseUrl, String hostUrl) {\n        this.mUrl = url;\n        this.mBaseUrl = baseUrl;\n        this.mHostUrl = hostUrl;\n        this.mSequence = 0;\n        this.mTsList = new ArrayList<>();\n    }\n\n    public void addTs(M3U8Ts ts) {\n        this.mTsList.add(ts);\n    }\n\n    public void setTargetDuration(float targetDuration) {\n        this.mTargetDuration = targetDuration;\n    }\n\n    public void setVersion(int version) {\n        this.mVersion = version;\n    }\n\n    public void setSequence(int sequence) { this.mSequence = sequence; }\n\n    public void setHasEndList(boolean hasEndList) {\n        this.mHasEndList = hasEndList;\n    }\n\n    public List<M3U8Ts> getTsList() {\n        return mTsList;\n    }\n\n    public int getVersion() {\n        return mVersion;\n    }\n\n    public float getTargetDuration() {\n        return mTargetDuration;\n    }\n\n    public int getSequence() {\n        return mSequence;\n    }\n\n    public boolean hasEndList() {\n        return mHasEndList;\n    }\n\n    public long getDuration() {\n        long duration = 0L;\n        for (M3U8Ts ts : mTsList) {\n            duration += ts.getDuration();\n        }\n        return duration;\n    }\n\n    public long getDuration(int tsIndex) {\n        if (tsIndex < 0 || mTsList.size() <= 0) {\n            return 0;\n        } else if (tsIndex >= mTsList.size() - 1) {\n            return getDuration();\n        } else {\n            long duration = 0L;\n            for (int index = 0; index <= tsIndex; index++) {\n                duration += mTsList.get(index).getDuration();\n            }\n            return duration;\n        }\n    }\n\n    public int getTsIndex(long playDuration) {\n        long duration = 0L;\n        int index = 0;\n        for (M3U8Ts ts : mTsList) {\n            if (playDuration >= duration\n                    && playDuration < duration + ts.getDuration()) {\n                return index;\n            }\n            duration += ts.getDuration();\n            index++;\n        }\n        return 0;\n    }\n\n    //Figure it out about the cached size between fromIndex and endIndex.\n    public long getCachedSizeFromIndex(int fromIndex, int endIndex) {\n        if (mTsList.size() <= 0 || fromIndex < 0 ||\n                fromIndex >= mTsList.size() - 1 || endIndex < 0)\n            return 0;\n        if (fromIndex > endIndex) {\n            return 0;\n        }\n        if (endIndex > mTsList.size() - 1) {\n            endIndex = mTsList.size() - 1;\n        }\n        long cachedSize = 0L;\n        for (int index = fromIndex; index <= endIndex; index++) {\n            M3U8Ts ts = mTsList.get(index);\n            cachedSize += ts.getTsSize();\n        }\n        return cachedSize;\n    }\n\n    public void setCurTsIndex(int curTsIndex) {\n        this.mCurTsIndex = curTsIndex;\n    }\n\n    public int getCurTsIndex() {\n        return mCurTsIndex;\n    }\n\n    public void printM3U8Info() {\n        LogUtils.i( \"M3U8 Url=\"+mUrl);\n        LogUtils.i(\"M3U8 BaseUrl=\"+mBaseUrl);\n        LogUtils.i(\"M3U8 HostUrl=\"+mHostUrl);\n        printTsInfo();\n    }\n\n    public void printTsInfo() {\n        for (int index=0; index < mTsList.size(); index++) {\n            LogUtils.i(\"\" + mTsList.get(index));\n        }\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof M3U8){\n            M3U8 m3u8 = (M3U8)obj;\n            if (mUrl != null && mUrl.equals(m3u8.mUrl))\n                return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/hls/M3U8Constants.java",
    "content": "package com.media.cache.hls;\n\nimport java.util.regex.Pattern;\n\npublic class M3U8Constants {\n\n    //base hls tag:\n\n    public static final String PLAYLIST_HEADER = \"#EXTM3U\"; //must\n    public static final String TAG_PREFIX = \"#EXT\";  //must\n    public static final String TAG_VERSION = \"#EXT-X-VERSION\";  //must\n    public static final String TAG_MEDIA_SEQUENCE = \"#EXT-X-MEDIA-SEQUENCE\";  //must\n    public static final String TAG_TARGET_DURATION = \"#EXT-X-TARGETDURATION\"; //must\n    public static final String TAG_MEDIA_DURATION = \"#EXTINF\"; //must\n    public static final String TAG_DISCONTINUITY = \"#EXT-X-DISCONTINUITY\"; // Optional\n    public static final String TAG_ENDLIST = \"#EXT-X-ENDLIST\";   //It is not live if hls has '#EXT-X-ENDLIST' tag; Or it is.\n    public static final String TAG_KEY = \"#EXT-X-KEY\";   //Optional\n\n    //extra hls tag:\n\n    // #EXT-X-PLAYLIST-TYPE:VOD       is not live\n    // #EXT-X-PLAYLIST-TYPE:EVENT   is live, we also can try '#EXT-X-ENDLIST'\n    public static final String TAG_PLAYLIST_TYPE = \"#EXT-X-PLAYLIST-TYPE\";\n    public static final String TAG_STREAM_INF = \"#EXT-X-STREAM-INF\";  //Multiple m3u8 stream, we usually fetch the first.\n    public static final String TAG_ALLOW_CACHE = \"EXT-X-ALLOW-CACHE\";  // YES : not live; NO: live\n\n    public static final Pattern REGEX_TARGET_DURATION =\n            Pattern.compile(TAG_TARGET_DURATION + \":(\\\\d+)\\\\b\");\n    public static final Pattern REGEX_MEDIA_DURATION =\n            Pattern.compile(TAG_MEDIA_DURATION + \":([\\\\d\\\\.]+)\\\\b\");\n    public static final Pattern REGEX_VERSION =\n            Pattern.compile(TAG_VERSION + \":(\\\\d+)\\\\b\");\n    public static final Pattern REGEX_MEDIA_SEQUENCE =\n            Pattern.compile(TAG_MEDIA_SEQUENCE + \":(\\\\d+)\\\\b\");\n\n    public static final String METHOD_NONE = \"NONE\";\n    public static final String METHOD_AES_128 = \"AES-128\";\n    public static final String METHOD_SAMPLE_AES = \"SAMPLE-AES\";\n    // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.\n    public static final String METHOD_SAMPLE_AES_CENC = \"SAMPLE-AES-CENC\";\n    public static final String METHOD_SAMPLE_AES_CTR = \"SAMPLE-AES-CTR\";\n    public static final Pattern REGEX_METHOD =\n            Pattern.compile(\"METHOD=(\" + METHOD_NONE + \"|\" + METHOD_AES_128 + \"|\" +\n                    METHOD_SAMPLE_AES + \"|\" + METHOD_SAMPLE_AES_CENC + \"|\" +\n                    METHOD_SAMPLE_AES_CTR + \")\"\n                    + \"\\\\s*(,|$)\");\n    public static final Pattern REGEX_KEYFORMAT =\n            Pattern.compile(\"KEYFORMAT=\\\"(.+?)\\\"\");\n    public static final Pattern REGEX_URI = Pattern.compile(\"URI=\\\"(.+?)\\\"\");\n    public static final Pattern REGEX_IV = Pattern.compile(\"IV=([^,.*]+)\");\n    public static final String KEYFORMAT_IDENTITY = \"identity\";\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/hls/M3U8Ts.java",
    "content": "package com.media.cache.hls;\n\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.util.Locale;\n\npublic class M3U8Ts implements Comparable<M3U8Ts> {\n\n    private float mDuration;\n    private int mIndex;\n    private String mUrl;\n    private String mName;\n    private long mTsSize;\n    private boolean mHasDiscontinuity;\n    private boolean mHasKey;\n    private String mMethod;\n    private String mKeyUri;\n    private String mKeyIV;\n    private boolean mIsMessyKey;\n\n    public M3U8Ts() {\n    }\n\n    public void initTsAttributes(String url, float duration, int index, boolean hasDiscontinuity, boolean hasKey) {\n        this.mUrl = url;\n        this.mName = url;\n        this.mDuration = duration;\n        this.mIndex = index;\n        this.mHasDiscontinuity = hasDiscontinuity;\n        this.mHasKey = hasKey;\n        this.mTsSize = 0L;\n    }\n\n    public void setKeyConfig(String method, String keyUri, String keyIV) {\n        this.mMethod = method;\n        this.mKeyUri = keyUri;\n        this.mKeyIV = keyIV;\n    }\n\n    public boolean hasKey() {\n        return mHasKey;\n    }\n\n    public String getMethod() {\n        return mMethod;\n    }\n\n    public String getKeyUri() {\n        return mKeyUri;\n    }\n\n    public String getLocalKeyUri() {\n        return \"local.key\";\n    }\n\n    public String getKeyIV() {\n        return mKeyIV;\n    }\n\n    public float getDuration() {\n        return mDuration;\n    }\n\n    public String getUrl() {\n        return mUrl;\n    }\n\n    public String getName() {\n        return mName;\n    }\n\n    /**\n     * if ts is local file, name is video_{index}.ts\n     * if ts is network resource , name is starting with http or https.\n     * @param name\n     */\n    public void setName(String name) {\n        this.mName = name;\n    }\n\n    public String getIndexName() {\n        return \"seg_\" + mIndex+\".ts\";\n    }\n\n    public String getProxyUrl(String host, int port, String fileName) {\n        return String.format(Locale.US, \"http://%s:%d/%s%s/%s/%s\", host, port, LocalProxyUtils.encodeUri(mUrl), LocalProxyUtils.SPLIT_STR, fileName, getIndexName());\n    }\n\n    public void setTsSize(long tsSize) {\n        this.mTsSize = tsSize;\n    }\n\n    public long getTsSize() {\n        return mTsSize;\n    }\n\n    public boolean hasDiscontinuity() {\n        return mHasDiscontinuity;\n    }\n\n    public void setIsMessyKey(boolean isMessyKey) {\n        this.mIsMessyKey = isMessyKey;\n    }\n\n    public boolean isMessyKey() {\n        return mIsMessyKey;\n    }\n\n    public String toString() {\n        return \"duration=\"+mDuration+\", index=\"+mIndex+\", name=\"+mName;\n    }\n\n    @Override\n    public int compareTo(M3U8Ts object) {\n        return mName.compareTo(object.mName);\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/hls/M3U8Utils.java",
    "content": "package com.media.cache.hls;\n\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.VideoCacheException;\nimport com.media.cache.utils.DownloadExceptionUtils;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport javax.net.ssl.HttpsURLConnection;\n\npublic class M3U8Utils {\n\n    private static final String TAG = \"M3U8Utils\";\n    /**\n     * parse M3U8 file.\n     * @param videoUrl\n     * @return\n     * @throws IOException\n     */\n    public static M3U8 parseM3U8Info(LocalProxyConfig config, String videoUrl, boolean isLocalFile, File m3u8File) throws IOException, VideoCacheException {\n        LogUtils.d(TAG + \" parseM3U8Info url=\"+videoUrl+\", isLocalFile=\"+isLocalFile);\n        URL url = new URL(videoUrl);\n        InputStreamReader inputStreamReader = null;\n        BufferedReader bufferedReader;\n        if (isLocalFile) {\n            inputStreamReader = new InputStreamReader(new FileInputStream(m3u8File));\n            bufferedReader = new BufferedReader(inputStreamReader);\n        } else {\n            HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n            if (config.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n                HttpUtils.trustAllCert((HttpsURLConnection)connection);\n            }\n            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));\n        }\n        String baseUriPath = videoUrl.substring(0, videoUrl.lastIndexOf(\"/\") + 1);\n        String hostUrl = videoUrl.substring(0, videoUrl.indexOf(url.getPath()) + 1);\n        M3U8 m3u8 = new M3U8(videoUrl, baseUriPath, hostUrl);\n        float tsDuration = 0;\n        int targetDuration = 0;\n        int tsIndex = 0;\n        int version = 0;\n        int sequence = 0;\n        boolean hasDiscontinuity = false;\n        boolean hasEndList = false;\n        boolean hasKey = false;\n        String method = null;\n        String encryptionIV = null;\n        String encryptionKeyUri = null;\n        String line;\n        boolean isM3U8 = false;\n        while ((line = bufferedReader.readLine()) != null) {\n            LogUtils.w(TAG + \" parseM3U8Info line=\"+line);\n            if (line.startsWith(M3U8Constants.TAG_PREFIX)) {\n                isM3U8 = true;\n                if (line.startsWith(M3U8Constants.TAG_MEDIA_DURATION)) {\n                    String ret = parseStringAttr(line, M3U8Constants.REGEX_MEDIA_DURATION);\n                    if (!TextUtils.isEmpty(ret)) {\n                        tsDuration = Float.parseFloat(ret);\n                    }\n                } else if (line.startsWith(M3U8Constants.TAG_TARGET_DURATION)) {\n                    String ret = parseStringAttr(line, M3U8Constants.REGEX_TARGET_DURATION);\n                    if (!TextUtils.isEmpty(ret)) {\n                        targetDuration = Integer.parseInt(ret);\n                    }\n                } else if (line.startsWith(M3U8Constants.TAG_VERSION)) {\n                    String ret = parseStringAttr(line, M3U8Constants.REGEX_VERSION);\n                    if (!TextUtils.isEmpty(ret)) {\n                        version = Integer.parseInt(ret);\n                    }\n                } else if (line.startsWith(M3U8Constants.TAG_MEDIA_SEQUENCE)) {\n                    String ret = parseStringAttr(line, M3U8Constants.REGEX_MEDIA_SEQUENCE);\n                    if (!TextUtils.isEmpty(ret)) {\n                        sequence = Integer.parseInt(ret);\n                    }\n                } else if (line.startsWith(M3U8Constants.TAG_DISCONTINUITY)) {\n                    hasDiscontinuity = true;\n                } else if (line.startsWith(M3U8Constants.TAG_ENDLIST)) {\n                    hasEndList = true;\n                } else if (line.startsWith(M3U8Constants.TAG_KEY)) {\n                    hasKey = true;\n                    method = parseOptionalStringAttr(line, M3U8Constants.REGEX_METHOD);\n                    String keyFormat = parseOptionalStringAttr(line, M3U8Constants.REGEX_KEYFORMAT);\n                    if (!M3U8Constants.METHOD_NONE.equals(method)) {\n                        encryptionIV = parseOptionalStringAttr(line, M3U8Constants.REGEX_IV);\n                        if (M3U8Constants.KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) {\n                            if (M3U8Constants.METHOD_AES_128.equals(method)) {\n                                // The segment is fully encrypted using an identity key.\n                                String tempKeyUri = parseStringAttr(line, M3U8Constants.REGEX_URI);\n                                if (tempKeyUri != null) {\n                                    if (tempKeyUri.startsWith(\"/\")) {\n                                        int tempIndex = tempKeyUri.indexOf('/', 1);\n                                        String tempUrl = tempKeyUri.substring(0, tempIndex);\n                                        tempIndex = videoUrl.indexOf(tempUrl);\n                                        tempUrl = videoUrl.substring(0, tempIndex) + tempKeyUri;\n                                        encryptionKeyUri = tempUrl;\n                                    } else if (tempKeyUri.startsWith(\"http\") || tempKeyUri.startsWith(\"https\")) {\n                                        encryptionKeyUri = tempKeyUri;\n                                    } else {\n                                        encryptionKeyUri = baseUriPath + tempKeyUri;\n                                    }\n                                }\n                            } else {\n                                // Do nothing. Samples are encrypted using an identity key, but\n                                // this is not supported. Hopefully, a traditional DRM alternative\n                                // is also provided.\n                            }\n                        } else {\n                            //Do nothing.\n                        }\n                    }\n                }\n                continue;\n            } else if (!isM3U8) {\n                throw new VideoCacheException(DownloadExceptionUtils.M3U8_FILE_CONTENT_ERROR_STRING);\n            }\n            //It has '#EXT-X-STREAM-INF' tag;\n            if (line.endsWith(\".m3u8\")) {\n                if (line.startsWith(\"/\")) {\n                    int tempIndex = line.indexOf('/', 1);\n                    String tempUrl;\n                    if (tempIndex == -1) {\n                        tempUrl = baseUriPath + line.substring(1);\n                    } else {\n                        tempUrl = line.substring(0, tempIndex);\n                        tempIndex = videoUrl.indexOf(tempUrl);\n                        tempUrl = videoUrl.substring(0, tempIndex) + line;\n                    }\n                    return parseM3U8Info(config, tempUrl, isLocalFile, m3u8File);\n                }\n                if (line.startsWith(\"http\") || line.startsWith(\"https\")) {\n                    return parseM3U8Info(config, line, isLocalFile, m3u8File);\n                }\n                return parseM3U8Info(config, baseUriPath + line, isLocalFile, m3u8File);\n            }\n            M3U8Ts ts = new M3U8Ts();\n            if (isLocalFile) {\n                ts.initTsAttributes(line, tsDuration, tsIndex, hasDiscontinuity, hasKey);\n            } else if (line.startsWith(\"https\") || line.startsWith(\"http\")) {\n                ts.initTsAttributes(line, tsDuration, tsIndex, hasDiscontinuity, hasKey);\n            } else {\n                if (line.startsWith(\"/\")) {\n                    int tempIndex = line.indexOf('/', 1);\n                    String tempUrl;\n                    if (tempIndex == -1) {\n                        tempUrl = baseUriPath + line.substring(1);\n                    } else {\n                        tempUrl = line.substring(0, tempIndex);\n                        tempIndex = videoUrl.indexOf(tempUrl);\n                        if (tempIndex == -1) {\n                            tempUrl = baseUriPath + line.substring(1);\n                        } else {\n                            tempUrl = videoUrl.substring(0, tempIndex) + line;\n                        }\n                    }\n                    ts.initTsAttributes(tempUrl, tsDuration, tsIndex, hasDiscontinuity, hasKey);\n                } else {\n                    ts.initTsAttributes(baseUriPath + line, tsDuration, tsIndex, hasDiscontinuity, hasKey);\n                }\n            }\n            if (hasKey) {\n                ts.setKeyConfig(method, encryptionKeyUri, encryptionIV);\n            }\n            m3u8.addTs(ts);\n            tsIndex++;\n            tsDuration = 0;\n            hasDiscontinuity = false;\n            hasKey = false;\n            method = null;\n            encryptionKeyUri = null;\n            encryptionIV = null;\n        }\n        if (inputStreamReader != null) {\n            inputStreamReader.close();\n        }\n        if (bufferedReader != null) {\n            bufferedReader.close();\n        }\n        m3u8.setTargetDuration(targetDuration);\n        m3u8.setVersion(version);\n        m3u8.setSequence(sequence);\n        m3u8.setHasEndList(hasEndList);\n        return m3u8;\n    }\n\n    private static String parseStringAttr(String line, Pattern pattern) {\n        if (pattern == null)\n            return null;\n        Matcher matcher = pattern.matcher(line);\n        if (matcher.find() && matcher.groupCount() == 1) {\n            return matcher.group(1);\n        }\n        return null;\n    }\n\n    private static String parseOptionalStringAttr(String line, Pattern pattern) {\n        if (pattern == null)\n            return null;\n        Matcher matcher = pattern.matcher(line);\n        return matcher.find() ? matcher.group(1) : null;\n    }\n\n    public static void createRemoteM3U8(File dir, M3U8 m3u8) throws IOException {\n        File m3u8File = new File(dir, \"remote.m3u8\");\n        if (m3u8File.exists()) {\n            return;\n        }\n        BufferedWriter bfw = new BufferedWriter(new FileWriter(m3u8File, false));\n        bfw.write(M3U8Constants.PLAYLIST_HEADER + \"\\n\");\n        bfw.write(M3U8Constants.TAG_VERSION + \":\" + m3u8.getVersion() + \"\\n\");\n        bfw.write(M3U8Constants.TAG_MEDIA_SEQUENCE + \":\"+m3u8.getSequence()+\"\\n\");\n        bfw.write(M3U8Constants.TAG_TARGET_DURATION + \":\" + m3u8.getTargetDuration() + \"\\n\");\n        for (M3U8Ts m3u8Ts : m3u8.getTsList()) {\n            if (m3u8Ts.hasKey()) {\n                if (m3u8Ts.getMethod() != null) {\n                    String key = \"METHOD=\" + m3u8Ts.getMethod();\n                    if (m3u8Ts.getKeyUri() != null) {\n                        String keyUri = m3u8Ts.getKeyUri();\n                        key += \",URI=\\\"\" + keyUri + \"\\\"\";\n                        URL keyURL = new URL(keyUri);\n                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(keyURL.openStream()));\n                        StringBuilder textBuilder = new StringBuilder();\n                        String line = null;\n                        while ((line = bufferedReader.readLine()) != null) {\n                            textBuilder.append(line);\n                        }\n                        boolean isMessyStr = LocalProxyUtils.isMessyCode(textBuilder.toString());\n                        m3u8Ts.setIsMessyKey(isMessyStr);\n                        File keyFile = new File(dir, m3u8Ts.getLocalKeyUri());\n                        FileOutputStream outputStream = new FileOutputStream(keyFile);\n                        outputStream.write(textBuilder.toString().getBytes());\n                        bufferedReader.close();\n                        outputStream.close();\n                        if (m3u8Ts.getKeyIV() != null) {\n                            key += \",IV=\" + m3u8Ts.getKeyIV();\n                        }\n                    }\n                    bfw.write(M3U8Constants.TAG_KEY + \":\" + key + \"\\n\");\n                }\n            }\n            if (m3u8Ts.hasDiscontinuity()) {\n                bfw.write(M3U8Constants.TAG_DISCONTINUITY+\"\\n\");\n            }\n            bfw.write(M3U8Constants.TAG_MEDIA_DURATION + \":\" + m3u8Ts.getDuration()+\",\\n\");\n            bfw.write(m3u8Ts.getUrl());\n            bfw.newLine();\n        }\n        bfw.write(M3U8Constants.TAG_ENDLIST);\n        bfw.flush();\n        bfw.close();\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/ChunkedOutputStream.java",
    "content": "package com.media.cache.http;\n\nimport java.io.FilterOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\npublic class ChunkedOutputStream extends FilterOutputStream {\n\n    public ChunkedOutputStream(OutputStream outputStream) {\n        super(outputStream);\n    }\n\n    @Override\n    public void write(int b) throws IOException {\n        byte[] data = { (byte) b };\n        write(data, 0, 1);\n    }\n\n    @Override\n    public void write(byte[] b) throws IOException {\n        write(b, 0, b.length);\n    }\n\n    @Override\n    public void write(byte[] b, int off, int len) throws IOException {\n        if (len == 0)\n            return;\n        out.write(String.format(\"%x\\r\\n\", len).getBytes());\n        out.write(b, off, len);\n        out.write(\"\\r\\n\".getBytes());\n    }\n\n    public void finish() throws IOException {\n        out.write(\"0\\r\\n\\r\\n\".getBytes());\n    }\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/ContentType.java",
    "content": "package com.media.cache.http;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class ContentType {\n\n    private static final String ASCII_ENCODING = \"US-ASCII\";\n    private static final String MULTIPART_FORM_DATA_HEADER = \"multipart/form-data\";\n\n    private static final String CONTENT_REGEX = \"[ |\\t]*([^/^ ^;^,]+/[^ ^;^,]+)\";\n    private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE);\n\n    private static final String CHARSET_REGEX = \"[ |\\t]*(charset)[ |\\t]*=[ |\\t]*['|\\\"]?([^\\\"^'^;^,]*)['|\\\"]?\";\n    private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE);\n\n    private static final String BOUNDARY_REGEX = \"[ |\\t]*(boundary)[ |\\t]*=[ |\\t]*['|\\\"]?([^\\\"^'^;^,]*)['|\\\"]?\";\n    private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE);\n\n    private final String mHeader;\n    private final String mContentType;\n    private final String mEncoding;\n    private final String mBoundary;\n\n    public ContentType(String header) {\n        this.mHeader = header;\n        if (header != null) {\n            mContentType = getDetailFromContentHeader(header, MIME_PATTERN, \"\", 1);\n            mEncoding = getDetailFromContentHeader(header, CHARSET_PATTERN, null, 2);\n        } else {\n            mContentType = \"\";\n            mEncoding = \"UTF-8\";\n        }\n        if (MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(mContentType)) {\n            mBoundary = getDetailFromContentHeader(header, BOUNDARY_PATTERN, null, 2);\n        } else {\n            mBoundary = null;\n        }\n    }\n\n    private String getDetailFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue, int group) {\n        Matcher matcher = pattern.matcher(contentTypeHeader);\n        return matcher.find() ? matcher.group(group) : defaultValue;\n    }\n\n    public String getEncoding() {\n        return mEncoding == null ? ASCII_ENCODING : mEncoding;\n    }\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/HttpRequest.java",
    "content": "package com.media.cache.http;\n\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.InetAddress;\nimport java.net.SocketException;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.StringTokenizer;\n\nimport javax.net.ssl.SSLException;\n\npublic class HttpRequest {\n    private final BufferedInputStream mInputStream;\n    private final String mRemoteIP;\n    private final String mRemoteHostname;\n    private final HashMap<String, String> mHeaders;\n    private HashMap<String, String> mParams;\n    private Method mMethod;\n    private String mUri;\n    private String mProtocolVersion;\n    private boolean mKeepAlive;\n    private String mQueryParameter;\n\n    public HttpRequest(InputStream inputStream, InetAddress inetAddress) {\n        this.mInputStream = new BufferedInputStream(inputStream);\n\n        //isLoopbackAddress() : local address; 127.0.0.0 ~ 127.255.255.255\n        //isAnyLocalAddress() : normal address ?\n        this.mRemoteIP = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? \"127.0.0.1\" : inetAddress.getHostAddress();\n        this.mRemoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? \"localhost\" : inetAddress.getHostName();\n        this.mHeaders = new HashMap<String, String>();\n    }\n\n    public void parseRequest() throws Exception {\n        byte[] buf = new byte[LocalProxyUtils.DEFAULT_BUFFER_SIZE];\n        int splitByteIndex = 0;\n        int readLength = 0;\n\n        int read = -1;\n        this.mInputStream.mark(LocalProxyUtils.DEFAULT_BUFFER_SIZE);\n        try {\n            read = this.mInputStream.read(buf, 0, LocalProxyUtils.DEFAULT_BUFFER_SIZE);\n        } catch (SSLException e) {\n            throw e;\n        } catch (IOException e) {\n            LocalProxyUtils.close(this.mInputStream);\n            throw new SocketException(\"Socket Shutdown\");\n        }\n        if (read == -1) {\n            LocalProxyUtils.close(this.mInputStream);\n            throw new SocketException(\"Can't read inputStream\");\n        }\n        while (read > 0) {\n            readLength += read;\n            splitByteIndex = findResponseHeaderEnd(buf, readLength);\n            if (splitByteIndex > 0) {\n                break;\n            }\n            read = this.mInputStream.read(buf, readLength, LocalProxyUtils.DEFAULT_BUFFER_SIZE - readLength);\n        }\n\n        if (splitByteIndex < readLength) {\n            this.mInputStream.reset();\n            this.mInputStream.skip(splitByteIndex);\n        }\n\n        this.mParams = new HashMap<String, String>();\n        this.mHeaders.clear();\n\n        // Create a BufferedReader for parsing the header.\n        BufferedReader headerReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, readLength)));\n\n        // Decode the header into params and header java properties\n        Map<String, String> extraInfo = new HashMap<String, String>();\n        decodeHeader(headerReader, extraInfo, this.mParams, this.mHeaders);\n\n        if (null != this.mRemoteIP) {\n            this.mHeaders.put(\"remote-addr\", this.mRemoteIP);\n            this.mHeaders.put(\"http-client-ip\", this.mRemoteIP);\n        }\n\n        this.mMethod = Method.lookup(extraInfo.get(\"method\"));\n        if (this.mMethod == null) {\n            throw new Exception(\"BAD REQUEST: Syntax error. HTTP verb \" + extraInfo.get(\"method\") + \" unhandled.\");\n        }\n\n        this.mUri = extraInfo.get(\"uri\");\n\n        String connection = this.mHeaders.get(\"connection\");\n        mKeepAlive = \"HTTP/1.1\".equals(mProtocolVersion) &&\n                (connection == null || !connection.matches(\"(?i).*close.*\"));\n    }\n\n    //GET / HTTP/1.1\\r\\nHost: www.sina.com.cn\\r\\nConnection: close\\r\\n\\r\\n\n    //'\\r\\n\\r\\n'\n    private int findResponseHeaderEnd(final byte[] buf, int readLength) {\n        int splitByteIndex = 0;\n        while (splitByteIndex + 1 < readLength) {\n\n            // RFC2616\n            if (buf[splitByteIndex] == '\\r'\n                    && buf[splitByteIndex + 1] == '\\n'\n                    && splitByteIndex + 3 < readLength\n                    && buf[splitByteIndex + 2] == '\\r'\n                    && buf[splitByteIndex + 3] == '\\n') {\n                return splitByteIndex + 4;\n            }\n\n            // tolerance\n            if (buf[splitByteIndex] == '\\n' && buf[splitByteIndex + 1] == '\\n') {\n                return splitByteIndex + 2;\n            }\n            splitByteIndex++;\n        }\n        return 0;\n    }\n\n    private void decodeHeader(BufferedReader headerReader,\n                              Map<String, String> extraInfo,\n                              Map<String, String> params,\n                              Map<String, String> headers) throws Exception {\n        try {\n            // Read the request line\n            String readLine = headerReader.readLine();\n            if (readLine == null) {\n                return;\n            }\n\n            StringTokenizer st = new StringTokenizer(readLine);\n            if (!st.hasMoreTokens()) {\n                throw new Exception(\"Bad request, syntax error, correct format: GET /example/file.html\");\n            }\n\n            extraInfo.put(\"method\", st.nextToken());\n\n            if (!st.hasMoreTokens()) {\n                throw new Exception(\"Bad request, syntax error, correct format: GET /example/file.html\");\n            }\n\n            String uri = st.nextToken();\n\n            // Decode parameters from the URI\n            int questionMaskIndex = uri.indexOf('?');\n            if (questionMaskIndex >= 0 && questionMaskIndex < uri.length()) {\n                decodeParams(uri.substring(questionMaskIndex + 1), params);\n                uri = LocalProxyUtils.decodeUri(uri.substring(0, questionMaskIndex));\n            } else {\n                uri = LocalProxyUtils.decodeUri(uri);\n            }\n\n            // If there's another token, its protocol version,\n            // followed by HTTP headers.\n            // NOTE: this now forces header names lower case since they are\n            // case insensitive and vary by client.\n            if (st.hasMoreTokens()) {\n                mProtocolVersion = st.nextToken();\n            } else {\n                //default protocol version\n                mProtocolVersion = \"HTTP/1.1\";\n            }\n\n            //parse headers:\n            String line = headerReader.readLine();\n            while (line != null && !line.trim().isEmpty()) {\n                int index = line.indexOf(':');\n                if (index >= 0 && index < line.length()) {\n                    headers.put(line.substring(0, index).trim().\n                            toLowerCase(Locale.US), line.substring(index + 1).trim());\n                }\n                line = headerReader.readLine();\n            }\n\n            extraInfo.put(\"uri\", uri);\n        } catch (IOException e) {\n            throw new Exception( \"Parsing Header Exception: \" + e.getMessage(), e);\n        }\n    }\n\n    private void decodeParams(String params, Map<String, String> paramsMap) {\n        if (params == null) {\n            this.mQueryParameter = \"\";\n            return;\n        }\n\n        this.mQueryParameter = params;\n        StringTokenizer st = new StringTokenizer(params, \"&\");\n        while (st.hasMoreTokens()) {\n            String item = st.nextToken();\n            int index = item.indexOf('=');\n            if (index >= 0 && index < item.length()) {\n                paramsMap.put(LocalProxyUtils.decodeUri(item.substring(0, index)).trim(),\n                        LocalProxyUtils.decodeUri(item.substring(index + 1)));\n            } else {\n                paramsMap.put(LocalProxyUtils.decodeUri(item).trim(), \"\");\n            }\n        }\n    }\n\n    public String getMimeType() {\n        return \"video/mpeg\";\n    }\n\n    public String getProtocolVersion() {\n        return mProtocolVersion;\n    }\n\n    public String getUri(){\n        return String.valueOf(mUri);\n    }\n\n    public boolean keepAlive() {\n        return mKeepAlive;\n    }\n\n    public Method requestMethod() {\n        return mMethod;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/HttpResponse.java",
    "content": "package com.media.cache.http;\n\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.PrintWriter;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\nimport javax.net.ssl.HttpsURLConnection;\n\nimport static java.net.HttpURLConnection.HTTP_MOVED_PERM;\nimport static java.net.HttpURLConnection.HTTP_MOVED_TEMP;\nimport static java.net.HttpURLConnection.HTTP_SEE_OTHER;\n\npublic class HttpResponse {\n\n    private static String CONTENT_TYPE = \"Content-Type\";\n    private static String DATE = \"Date\";\n    private static String CONNECTION = \"Connection\";\n    private static String TRANSFER_ENCODING = \"Transfer-Encoding\";\n    private static String GMT_PATTERN = \"E, d MMM yyyy HH:mm:ss 'GMT'\";\n\n    private final HttpRequest mRequest;\n    private final LocalProxyConfig mConfig;\n    private final File mCacheRoot;\n    private final String mMimeType;\n    private final String mProtocolVersion;\n    private IState mResponseState;\n    private InputStream mInputStream;\n\n    public HttpResponse(HttpRequest request, LocalProxyConfig config) throws Exception {\n        this.mRequest = request;\n        this.mConfig = config;\n        this.mCacheRoot = config.getCacheRoot();\n        this.mMimeType = mRequest.getMimeType();\n        this.mProtocolVersion = mRequest.getProtocolVersion();\n        String resultUrl = mRequest.getUri();\n        if (resultUrl.startsWith(\"/http://\") || resultUrl.startsWith(\"/https://\")) {\n            resultUrl = resultUrl.substring(1);\n            if (resultUrl.contains(LocalProxyUtils.SPLIT_STR)) {\n                String[] arr = resultUrl.split(LocalProxyUtils.SPLIT_STR);\n                String url = arr[0];\n                String fileName = arr[1];\n\n                File file = new File(mCacheRoot, fileName);\n                if (file.exists()) {\n                    try {\n                        mInputStream = new FileInputStream(file);\n                        this.mResponseState = ResponseState.OK;\n                    } catch (Exception e) {\n                        throw new Exception(\"No files found to the request:\" + file.getAbsolutePath(), e);\n                    }\n                } else {\n                    try {\n                        mInputStream = downloadFile(url, file);\n                        this.mResponseState = ResponseState.OK;\n                    } catch (Exception e) {\n                        throw new Exception(\"HttpResponse download file failed:\"+e);\n                    }\n                }\n            }\n\n        } else {\n            File file = new File(mCacheRoot, mRequest.getUri());\n            LogUtils.w(\"jeffmony HttpResponse file exist=\"+file.exists());\n            if (file.exists()) {\n                try {\n                    mInputStream = new FileInputStream(file);\n                    this.mResponseState = ResponseState.OK;\n                } catch (Exception e) {\n                    throw new Exception(\"No files found to the request:\" + file.getAbsolutePath(), e);\n                }\n            } else {\n                mResponseState = ResponseState.INTERNAL_ERROR;\n                throw new Exception(\"No files found to the request:\" + file.getAbsolutePath());\n            }\n        }\n    }\n\n    private static final int REDIRECTED_COUNT = 3;\n\n    public InputStream downloadFile(String url, File file) throws Exception {\n        HttpURLConnection connection = null;\n        InputStream inputStream = null;\n        try {\n            connection = openConnection(url);\n            int responseCode = connection.getResponseCode();\n            if (responseCode == HttpUtils.RESPONSE_OK) {\n                inputStream = connection.getInputStream();\n                saveFile(inputStream, file);\n                return inputStream;\n            }\n        }catch (Exception e) {\n            throw e;\n        }finally {\n            if (connection != null)\n                connection.disconnect();\n        }\n        return null;\n    }\n\n    private HttpURLConnection openConnection(String videoUrl)\n            throws Exception {\n        HttpURLConnection connection;\n        boolean redirected;\n        int redirectedCount = 0;\n        do {\n            URL url = new URL(videoUrl);\n            connection = (HttpURLConnection)url.openConnection();\n            if (mConfig.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n                HttpUtils.trustAllCert((HttpsURLConnection)(connection));\n            }\n            connection.setConnectTimeout(mConfig.getConnTimeOut());\n            connection.setReadTimeout(mConfig.getReadTimeOut());\n            int code = connection.getResponseCode();\n            redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP ||\n                    code == HTTP_SEE_OTHER;\n            if (redirected) {\n                redirectedCount++;\n                connection.disconnect();\n            }\n            if (redirectedCount > REDIRECTED_COUNT) {\n                throw new Exception(\"Too many redirects: \" +\n                        redirectedCount);\n            }\n        } while (redirected);\n        return connection;\n    }\n\n    private void saveFile(InputStream inputStream, File file) {\n        FileOutputStream fos = null;\n        try {\n            fos = new FileOutputStream(file);\n            int len = 0;\n            byte[] buf = new byte[LocalProxyUtils.DEFAULT_BUFFER_SIZE];\n            while ((len = inputStream.read(buf)) != -1) {\n                fos.write(buf, 0, len);\n            }\n        } catch (Exception e) {\n            LogUtils.w(file.getAbsolutePath() + \" saveFile failed, exception=\"+e);\n            if (file.exists()) {\n                file.delete();\n            }\n        } finally {\n            LocalProxyUtils.close(inputStream);\n            LocalProxyUtils.close(fos);\n        }\n    }\n\n    public void send(OutputStream outputStream) throws Exception {\n        SimpleDateFormat gmtFormat= new SimpleDateFormat(GMT_PATTERN, Locale.US);\n        gmtFormat.setTimeZone(TimeZone.getTimeZone(\"GMT\"));\n        try {\n            if (mResponseState == null) {\n                throw new Exception(\"sendResponse(): Status can't be null.\");\n            }\n            PrintWriter pw = new PrintWriter(new BufferedWriter(new\n                    OutputStreamWriter(outputStream, new ContentType(mMimeType).getEncoding())), false);\n            if (TextUtils.isEmpty(mProtocolVersion)) {\n                pw.append(\"HTTP/1.1 \");\n            } else {\n                pw.append(mProtocolVersion + \" \");\n            }\n            pw.append(mResponseState.getDescription()).append(\" \\r\\n\");\n            if (!TextUtils.isEmpty(mMimeType)) {\n                appendHeader(pw, CONTENT_TYPE, mMimeType);\n            }\n            appendHeader(pw, DATE, gmtFormat.format(new Date()));\n            appendHeader(pw, CONNECTION, ( mRequest.keepAlive() ? \"keep-alive\" : \"close\"));\n            if (mRequest.requestMethod() != Method.HEAD ) {\n                appendHeader(pw, TRANSFER_ENCODING, \"chunked\");\n            }\n            pw.append(\"\\r\\n\");\n            pw.flush();\n            sendBodyWithCorrectTransferAndEncoding(outputStream);\n            outputStream.flush();\n        } catch (IOException e) {\n            throw new Exception(\"send response failed: \", e);\n        } finally {\n            LocalProxyUtils.close(this.mInputStream);\n        }\n    }\n\n    protected void appendHeader(PrintWriter pw, String key, String value) {\n//        LogUtils.i(\"HttpResponse--[printHeader] key=\"+key+\" value=\"+value);\n        pw.append(key).append(\": \").append(value).append(\"\\r\\n\");\n    }\n\n    private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream) throws IOException {\n        ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);\n        sendBody(chunkedOutputStream, -1);\n        chunkedOutputStream.finish();\n    }\n\n    private void sendBody(OutputStream outputStream, long pending) throws IOException {\n        long buffer_size = LocalProxyUtils.DEFAULT_BUFFER_SIZE;\n        byte[] buff = new byte[(int) buffer_size];\n        boolean sendEverything = pending == -1;\n        while (pending > 0 || sendEverything) {\n            long bytesToRead = sendEverything ? buffer_size : Math.min(pending, buffer_size);\n            if (this.mInputStream == null) {\n                break;\n            }\n            int read = this.mInputStream.read(buff, 0, (int) bytesToRead);\n            if (read <= 0) {\n                break;\n            }\n            outputStream.write(buff, 0, read);\n            if (!sendEverything) {\n                pending -= read;\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/IState.java",
    "content": "package com.media.cache.http;\n\npublic interface IState {\n\n    String getDescription();\n\n    int getResponseCode();\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/Method.java",
    "content": "package com.media.cache.http;\n\npublic enum  Method {\n    GET,\n    PUT,\n    POST,\n    DELETE,\n    HEAD,\n    OPTIONS,\n    TRACE,\n    CONNECT,\n    PATCH,\n    PROPFIND,\n    PROPPATCH,\n    MKCOL,\n    MOVE,\n    COPY,\n    LOCK,\n    UNLOCK;\n\n    public static Method lookup(String method) {\n        if (method == null)\n            return null;\n        try {\n            return valueOf(method);\n        } catch (IllegalArgumentException e) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/ResponseState.java",
    "content": "package com.media.cache.http;\n\npublic enum ResponseState implements IState {\n\n    SWITCH_PROTOCOL(101, \"Switching Protocols\"),\n\n    OK(200, \"OK\"),\n    CREATED(201, \"Created\"),\n    ACCEPTED(202, \"Accepted\"),\n    NO_CONTENT(204, \"No Content\"),\n    PARTIAL_CONTENT(206, \"Partial Content\"),\n    MULTI_STATUS(207, \"Multi-Status\"),\n\n    REDIRECT(301, \"Moved Permanently\"),\n    /**\n     * Many user agents mishandle 302 in ways that violate the RFC1945 spec (i.e., redirect a POST to a GET).\n     * 303 and 307 were added in RFC2616 to address this.  You should prefer 303 and 307 unless the calling\n     * user agent does not support 303 and 307 functionality\n     */\n    @Deprecated\n    FOUND(302, \"Found\"),\n    REDIRECT_SEE_OTHER(303, \"See Other\"),\n    NOT_MODIFIED(304, \"Not Modified\"),\n    TEMPORARY_REDIRECT(307,\"Temporary Redirect\"),\n\n    BAD_REQUEST(400, \"Bad Request\"),\n    UNAUTHORIZED(401, \"Unauthorized\"),\n    FORBIDDEN(403, \"Forbidden\"),\n    NOT_FOUND(404, \"Not Found\"),\n    METHOD_NOT_ALLOWED(405, \"Method Not Allowed\"),\n    NOT_ACCEPTABLE(406, \"Not Acceptable\"),\n    REQUEST_TIMEOUT(408, \"Request Timeout\"),\n    CONFLICT(409, \"Conflict\"),\n    GONE(410, \"Gone\"),\n    LENGTH_REQUIRED(411, \"Length Required\"),\n    PRECONDITION_FAILED(412, \"Precondition Failed\"),\n    PAYLOAD_TOO_LARGE(413, \"Payload Too Large\"),\n    UNSUPPORTED_MEDIA_TYPE(415, \"Unsupported Media Type\"),\n    RANGE_NOT_SATISFIABLE(416, \"Requested Range Not Satisfiable\"),\n    EXPECTATION_FAILED(417, \"Expectation Failed\"),\n    TOO_MANY_REQUESTS(429, \"Too Many Requests\"),\n\n    INTERNAL_ERROR(500, \"Internal Server Error\"),\n    NOT_IMPLEMENTED(501, \"Not Implemented\"),\n    SERVICE_UNAVAILABLE(503, \"Service Unavailable\"),\n    UNSUPPORTED_HTTP_VERSION(505, \"HTTP Version Not Supported\");\n\n    private final int mResponseCode;\n\n    private final String mResponseDescription;\n\n    ResponseState(int responseCode, String description) {\n        this.mResponseCode = responseCode;\n        this.mResponseDescription = description;\n    }\n\n    public static ResponseState lookup(int responseCode) {\n        for (ResponseState state : ResponseState.values()) {\n            if (state.getResponseCode() == responseCode) {\n                return state;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String getDescription() {\n        return \"\" + this.mResponseCode + \" \" + this.mResponseDescription;\n    }\n\n    @Override\n    public int getResponseCode() {\n        return this.mResponseCode;\n    }\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/http/SocketProcessorTask.java",
    "content": "package com.media.cache.http;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.Socket;\n\npublic class SocketProcessorTask implements Runnable {\n\n    private final LocalProxyConfig mConfig;\n    private final Socket mSocket;\n\n    public SocketProcessorTask(Socket socket, LocalProxyConfig config) {\n        this.mConfig = config;\n        this.mSocket = socket;\n    }\n\n    @Override\n    public void run() {\n        OutputStream outputStream = null;\n        InputStream inputStream = null;\n        try {\n            outputStream = mSocket.getOutputStream();\n            inputStream = mSocket.getInputStream();\n\n            HttpRequest request = new HttpRequest(inputStream, mSocket.getInetAddress());\n            while (!mSocket.isClosed()) {\n                request.parseRequest();\n                HttpResponse response = new HttpResponse(request, mConfig);\n                response.send(outputStream);\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"socket request failed, exception=\" + e);\n        } finally {\n            LocalProxyUtils.close(outputStream);\n            LocalProxyUtils.close(inputStream);\n            LocalProxyUtils.close(mSocket);\n        }\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/listener/IDownloadInfosCallback.java",
    "content": "package com.media.cache.listener;\n\nimport com.media.cache.model.VideoTaskItem;\n\nimport java.util.List;\n\npublic interface IDownloadInfosCallback {\n\n    void onDownloadInfos(List<VideoTaskItem> items);\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/listener/IDownloadListener.java",
    "content": "package com.media.cache.listener;\n\nimport com.media.cache.model.VideoTaskItem;\n\npublic interface IDownloadListener {\n\n    void onDownloadDefault(VideoTaskItem item);\n\n    void onDownloadPrepare(VideoTaskItem item);\n\n    void onDownloadPending(VideoTaskItem item);\n\n    void onDownloadStart(VideoTaskItem item);\n\n    void onDownloadProxyReady(VideoTaskItem item);\n\n    void onDownloadProgress(VideoTaskItem item);\n\n    void onDownloadSpeed(VideoTaskItem item);\n\n    void onDownloadPause(VideoTaskItem item);\n\n    void onDownloadError(VideoTaskItem item);\n\n    void onDownloadProxyForbidden(VideoTaskItem item);\n\n    void onDownloadSuccess(VideoTaskItem item);\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/listener/IDownloadTaskListener.java",
    "content": "package com.media.cache.listener;\n\nimport com.media.cache.hls.M3U8;\n\npublic interface IDownloadTaskListener {\n\n    void onTaskStart(String url);\n\n    void onLocalProxyReady(String proxyUrl);\n\n    void onTaskProgress(float percent, long cachedSize, M3U8 m3u8);\n\n    void onTaskSpeedChanged(float speed);\n\n    void onTaskPaused();\n\n    void onTaskFinished(long totalSize);\n\n    void onTaskFailed(Throwable e);\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/listener/IVideoInfoCallback.java",
    "content": "package com.media.cache.listener;\n\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.hls.M3U8;\n\npublic interface IVideoInfoCallback {\n\n    void onFinalUrl(String finalUrl);\n\n    void onBaseVideoInfoSuccess(VideoCacheInfo info);\n\n    void onBaseVideoInfoFailed(Throwable error);\n\n    void onM3U8InfoSuccess(VideoCacheInfo info, M3U8 m3u8);\n\n    void onLiveM3U8Callback(VideoCacheInfo info);\n\n    void onM3U8InfoFailed(Throwable error);\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/listener/IVideoInfoParseCallback.java",
    "content": "package com.media.cache.listener;\n\nimport com.media.cache.model.VideoCacheInfo;\nimport com.media.cache.hls.M3U8;\n\npublic interface IVideoInfoParseCallback {\n\n    void onM3U8FileParseSuccess(VideoCacheInfo info, M3U8 m3u8);\n\n    void onM3U8FileParseFailed(VideoCacheInfo info, Throwable error);\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/model/Video.java",
    "content": "package com.media.cache.model;\n\npublic class Video {\n\n    public static class Type {\n        public static final int HLS_TYPE = 0x1;\n        public static final int HLS_LIVE_TYPE = 0x2;\n        public static final int MP4_TYPE = 0x3;\n        public static final int WEBM_TYPE = 0x4;\n        public static final int QUICKTIME_TYPE = 0x5;\n        public static final int GP3_TYPE = 0x6;\n        public static final int MP3_TYPE = 0x7;\n    }\n\n    public static class Mime {\n\n        public static String MIME_TYPE_MP4 = \"video/mp4\";\n        public static String MIME_TYPE_M3U8_1 = \"application/vnd.apple.mpegurl\";\n        public static String MIME_TYPE_M3U8_2 = \"application/x-mpegurl\";\n        public static String MIME_TYPE_M3U8_3 = \"vnd.apple.mpegurl\";\n        public static String MIME_TYPE_M3U8_4 = \"applicationnd.apple.mpegurl\";\n\n        //Test url: https://vmedia.trafforsrv.com/system/files/videos/25147/t_f90367ccd2c15b649facea2b8008d450.webm\n        public static String MIME_TYPE_WEBM = \"video/webm\";\n\n        //Test url: https://vdse.bdstatic.com/3805e7089388e9abcc7fc59029f9363c.mov\n        public static String MIME_TYPE_QUICKTIME = \"video/quicktime\";\n\n        //Test url: https://x13y5.qq360cn.com/xx/file/774303/83113afba440817fe0584f917aefc660.3gp\n        public static String MIME_TYPE_3GP = \"video/3gp\";\n\n        //Test urls:\n        //1.https://api.37live.com/api/ngyun/index.php?vid=We2egMd6z3owhm8LjOO0OOOgpQ0O0O00O0O0&hd=m3u8      ignore cert example;\n        public static String MIME_TYPE_MP3 = \"audio/mpeg\";\n    }\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/model/VideoCacheInfo.java",
    "content": "package com.media.cache.model;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\n\npublic class VideoCacheInfo implements Serializable {\n\n    private String mUrl; // Orignal url\n    private String mFinalUrl; // Final url by redirecting.\n    private boolean mIsCompleted;\n    private int mType;\n    private long mCachedLength;\n    private long mTotalLength;\n    private int mCachedTs;\n    private int mTotalTs;\n    private String mSaveDir;\n    private LinkedHashMap<Long, Long> mSegmentList; // save the video segements' info.\n    private int mPort;\n    private int mTaskMode;\n    private float mPercent;\n    private long mDownloadTime;\n\n    public VideoCacheInfo(String videoUrl) {\n        super();\n        mUrl = videoUrl;\n        mTotalLength = -1L;\n        mType = -1;\n        mSegmentList = new LinkedHashMap<>();\n        mDownloadTime = 0L;\n    }\n\n    public void setUrl(String videoUrl) {\n        mUrl = videoUrl;\n    }\n\n    public String getUrl() {\n        return mUrl;\n    }\n\n    public void setFinalUrl(String finalUrl) { this.mFinalUrl = finalUrl; }\n\n    public String getFinalUrl() { return mFinalUrl; }\n\n    public void setIsCompleted(boolean isCompleted) {\n        this.mIsCompleted = isCompleted;\n    }\n\n    public boolean getIsCompleted() {\n        return mIsCompleted;\n    }\n\n    public void setVideoType(int videoType) {\n        this.mType = videoType;\n    }\n\n    public int getVideoType() {\n        return mType;\n    }\n\n    public void setCachedLength(long cachedLength) {\n        this.mCachedLength = cachedLength;\n    }\n\n    public long getCachedLength() {\n        return mCachedLength;\n    }\n\n    public void setTotalLength(long totalLength) {\n        this.mTotalLength = totalLength;\n    }\n\n    public long getTotalLength() {\n        return mTotalLength;\n    }\n\n    public void setCachedTs(int cachedTs) {\n        this.mCachedTs = cachedTs;\n    }\n\n    public int getCachedTs() {\n        return mCachedTs;\n    }\n\n    public void setTotalTs(int totalTs) {\n        this.mTotalTs = totalTs;\n    }\n\n    public int getTotalTs() {\n        return mTotalTs;\n    }\n\n    public void setSaveDir(String saveDir) {\n        this.mSaveDir = saveDir;\n    }\n\n    public String getSaveDir() {\n        return mSaveDir;\n    }\n\n    public void setPort(int port) { mPort = port; }\n\n    public int getPort() { return mPort; }\n\n    public void setTaskMode(int mode) {\n        mTaskMode = mode;\n    }\n\n    public int getTaskMode() {\n        return mTaskMode;\n    }\n\n    public void setPercent(float percent) { mPercent = percent; }\n\n    public float getPercent() { return mPercent; }\n\n    public void setSegmentList(LinkedHashMap<Long, Long> list) {\n        this.mSegmentList = list;\n    }\n\n    public LinkedHashMap<Long, Long> getSegmentList() {\n        return mSegmentList;\n    }\n\n    public void setDownloadTime(long time) {\n        mDownloadTime = time;\n    }\n\n    public long getDownloadTime() {\n        return mDownloadTime;\n    }\n\n    public String toString() {\n        return \"VideoCacheInfo[url=\"+mUrl+\", complete=\"+mIsCompleted+\", type=\"+mType+\", downloadTime=\"+mDownloadTime\n                +\", cachedLength=\"+mCachedLength+\", totalLength=\" +mTotalLength+\", cachedTs=\"+mCachedTs\n                +\", totalTs=\"+mTotalTs+\", saveDir=\"+mSaveDir+\", segmentSize=\" + mSegmentList.size() +\"]\";\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/model/VideoTaskItem.java",
    "content": "package com.media.cache.model;\n\nimport androidx.annotation.Nullable;\n\nimport com.android.baselib.utils.Utility;\nimport com.media.cache.hls.M3U8;\n\npublic class VideoTaskItem {\n\n    private String mUrl;\n    private String mProxyUrl;\n    private boolean mProxyReady;\n    private M3U8 mM3U8;\n    private float mSpeed;\n    private float mPercent;\n    private long mDownloadSize;\n    private int mVideoType;\n    private int mTaskState;\n    private int mTaskMode;\n    private long mDownloadTime;\n    private int mErrorCode;\n\n    public VideoTaskItem(String url, int mode) {\n        mUrl = url;\n        mTaskMode = mode;\n        mTaskState = VideoTaskState.DEFAULT;\n    }\n\n    public String getUrl() {\n        return mUrl;\n    }\n\n    public void setProxyUrl(String proxyUrl) {\n        mProxyUrl = proxyUrl;\n        mProxyReady = true;\n    }\n\n    public String getProxyUrl() {\n        return mProxyUrl;\n    }\n\n    public boolean getProxyReady() {\n        return mProxyReady;\n    }\n\n    public void setM3U8(M3U8 m3u8) {\n        mM3U8 = m3u8;\n    }\n\n    public M3U8 getM3U8() {\n        return mM3U8;\n    }\n\n    public void setSpeed(float speed) {\n        mSpeed = speed;\n    }\n\n    public float getSpeed() {\n        return mSpeed;\n    }\n\n    public String getSpeedString() {\n        return Utility.getSize((long)mSpeed) + \"/s\";\n    }\n\n    public void setPercent(float percent) {\n        mPercent = percent;\n    }\n\n    public float getPercent() {\n        return mPercent;\n    }\n\n    public String getPercentString() {\n        return Utility.getPercent(mPercent);\n    }\n\n    public void setDownloadSize(long size) {\n        mDownloadSize = size;\n    }\n\n    public long getDownloadSize() {\n        return mDownloadSize;\n    }\n\n    public String getDownloadSizeString() {\n        return Utility.getSize(mDownloadSize);\n    }\n\n    public void setVideoType(int type) {\n        mVideoType = type;\n    }\n\n    public int getVideoType() {\n        return mVideoType;\n    }\n\n    public void setTaskState(int state) {\n        mTaskState = state;\n    }\n\n    public int getTaskState() {\n        return mTaskState;\n    }\n\n    public void setTaskMode(int mode) { mTaskMode = mode; }\n\n    public int getTaskMode() { return mTaskMode; }\n\n    public void setDownloadTime(long time) {\n        mDownloadTime = time;\n    }\n\n    public long getDownloadTime() {\n        return mDownloadTime;\n    }\n\n    public boolean isDownloadMode() {\n        return getTaskMode() == VideoTaskMode.DOWNLOAD_MODE;\n    }\n\n    public boolean isPlayMode() {\n        return getTaskMode() == VideoTaskMode.PLAY_MODE;\n    }\n\n    public boolean isRunningTask() {\n        return mTaskState == VideoTaskState.DOWNLOADING || mTaskMode == VideoTaskState.PROXYREADY;\n    }\n\n    public boolean isSlientTask() {\n        return mTaskState == VideoTaskState.DEFAULT || mTaskState == VideoTaskState.PAUSE || mTaskState == VideoTaskState.ERROR;\n    }\n\n    public void setErrorCode(int errorCode) {\n        mErrorCode = errorCode;\n    }\n\n    public int getErrorCode() {\n        return mErrorCode;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n        if (obj != null && obj instanceof VideoTaskItem) {\n            String objUrl = ((VideoTaskItem)obj).getUrl();\n            if (mUrl.equals(objUrl)) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/model/VideoTaskMode.java",
    "content": "package com.media.cache.model;\n\npublic class VideoTaskMode {\n  public static final int DEFAULT_MODE = 0x0;\n  public static final int DOWNLOAD_MODE = 0x1;\n  public static final int DOWNLOAD_PLAY_MODE = 0x2;\n  public static final int PLAY_DOWNLOAD_MODE = 0x3;\n  public static final int PLAY_MODE = 0x4;\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/model/VideoTaskState.java",
    "content": "package com.media.cache.model;\n\npublic class VideoTaskState {\n    public static final int DEFAULT = 0;//默认状态\n    public static final int PENDING = -1;//下载排队\n    public static final int PREPARE = 1;//下载准备中\n    public static final int START = 2;  //开始下载\n    public static final int DOWNLOADING = 3;//下载中\n    public static final int PROXYREADY = 4; //视频可以边下边播\n    public static final int SUCCESS = 5;//下载完成\n    public static final int ERROR = 6;//下载出错\n    public static final int PAUSE = 7;//下载暂停\n    public static final int ENOSPC = 8;//空间不足\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/proxy/AsyncProxyServer.java",
    "content": "package com.media.cache.proxy;\n\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.jeffmony.async.AsyncServer;\nimport com.jeffmony.async.AsyncServerSocket;\nimport com.jeffmony.async.http.server.AsyncHttpServer;\nimport com.jeffmony.async.http.server.AsyncHttpServerRequest;\nimport com.jeffmony.async.http.server.AsyncHttpServerResponse;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.utils.HttpUtils;\nimport com.media.cache.utils.LocalProxyUtils;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport javax.net.ssl.HttpsURLConnection;\n\nimport static java.net.HttpURLConnection.HTTP_MOVED_PERM;\nimport static java.net.HttpURLConnection.HTTP_MOVED_TEMP;\nimport static java.net.HttpURLConnection.HTTP_SEE_OTHER;\n\npublic class AsyncProxyServer {\n  private final AsyncHttpServer mHttpServer;\n  private final LocalProxyConfig mConfig;\n  private static final String PROXY_HOST = \"127.0.0.1\";\n  private static final int REDIRECTED_COUNT = 3;\n\n  public AsyncProxyServer(LocalProxyConfig config) {\n    mConfig = config;\n    mHttpServer = new AsyncHttpServer();\n\n    mHttpServer.setErrorCallback(e -> LogUtils.w(\"AsyncProxyServer.ErrorCallback.exception=\" + e));\n\n    mHttpServer.get(\"/.*\", (request, response) -> {\n      try {\n        sendData(request, response);\n      } catch (Exception e) {\n        e.printStackTrace();\n      }\n    });\n\n    AsyncServerSocket socket = mHttpServer.listen(0);\n    mConfig.setConfig(PROXY_HOST, socket.getLocalPort());\n  }\n\n  private void sendData(AsyncHttpServerRequest request, AsyncHttpServerResponse response) throws Exception {\n    String resultUrl = LocalProxyUtils.decodeUri(request.getPath());\n    if (TextUtils.isEmpty(resultUrl)) {\n      LogUtils.e(\"ProxyServer failed, sendData request url is null.\");\n      return;\n    }\n    resultUrl = resultUrl.substring(1);\n    if (resultUrl.startsWith(\"http://\") || resultUrl.startsWith(\"https://\")) {\n      if (resultUrl.contains(LocalProxyUtils.SPLIT_STR)) {\n        String[] arr = resultUrl.split(LocalProxyUtils.SPLIT_STR);\n        String videoUrl = arr[0];\n        String fileName = arr[1].substring(1);\n        File file = new File(mConfig.getCacheRoot(), fileName);\n        if (file.exists()) {\n          response.sendFile(file);\n        } else {\n          sendDataByNetwork(videoUrl, file, response);\n        }\n      }\n    } else {\n      File file = new File(mConfig.getCacheRoot(), resultUrl);\n      if (file.exists()) {\n        response.sendFile(file);\n      } else {\n        LogUtils.e(\"sendData failed, \"+ file.getAbsolutePath() + \" not found.\");\n      }\n    }\n  }\n\n  public void sendDataByNetwork(String url, File file, AsyncHttpServerResponse response) {\n    HttpURLConnection connection = null;\n    InputStream inputStream;\n    try {\n      connection = openConnection(url);\n      int responseCode = connection.getResponseCode();\n      int contentLength = connection.getContentLength();\n      if (responseCode == HttpUtils.RESPONSE_OK) {\n        inputStream = connection.getInputStream();\n        response.sendStream(inputStream, contentLength);\n        saveFile(inputStream, file);\n      }\n    } catch (Exception e) {\n      LogUtils.w(\"sendDataByNetwork failed, exception=\"+e.getMessage());\n    } finally {\n      if (connection != null)\n        connection.disconnect();\n    }\n  }\n\n  private HttpURLConnection openConnection(String videoUrl)\n          throws Exception {\n    HttpURLConnection connection;\n    boolean redirected;\n    int redirectedCount = 0;\n    do {\n      URL url = new URL(videoUrl);\n      connection = (HttpURLConnection)url.openConnection();\n      if (mConfig.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n        HttpUtils.trustAllCert((HttpsURLConnection)(connection));\n      }\n      connection.setConnectTimeout(mConfig.getConnTimeOut());\n      connection.setReadTimeout(mConfig.getReadTimeOut());\n      int code = connection.getResponseCode();\n      redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP ||\n              code == HTTP_SEE_OTHER;\n      if (redirected) {\n        redirectedCount++;\n        connection.disconnect();\n      }\n      if (redirectedCount > REDIRECTED_COUNT) {\n        throw new Exception(\"Too many redirects: \" + redirectedCount);\n      }\n    } while (redirected);\n    return connection;\n  }\n\n  private void saveFile(InputStream inputStream, File file) {\n    FileOutputStream fos = null;\n    try {\n      fos = new FileOutputStream(file);\n      int len = 0;\n      byte[] buf = new byte[LocalProxyUtils.DEFAULT_BUFFER_SIZE];\n      while ((len = inputStream.read(buf)) != -1) {\n        fos.write(buf, 0, len);\n      }\n    } catch (Exception e) {\n      LogUtils.w(file.getAbsolutePath() + \" saveFile failed, exception=\"+e);\n      if (file.exists()) {\n        file.delete();\n      }\n    } finally {\n      LocalProxyUtils.close(inputStream);\n      LocalProxyUtils.close(fos);\n    }\n  }\n\n  private void shutdown() {\n    mHttpServer.stop();\n    AsyncServer.getDefault().stop();\n  }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/proxy/CustomProxyServer.java",
    "content": "package com.media.cache.proxy;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\nimport com.media.cache.http.SocketProcessorTask;\n\nimport java.net.InetAddress;\nimport java.net.ServerSocket;\nimport java.net.Socket;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class CustomProxyServer {\n\n    private final ExecutorService mSocketPool = Executors.newFixedThreadPool(8);\n    private final LocalProxyConfig mConfig;\n    private static final String PROXY_HOST = \"127.0.0.1\";\n\n    private Thread mRequestThread;\n    private ServerSocket mServerSocket;\n    private int mPort;\n\n    public CustomProxyServer(LocalProxyConfig config) {\n        mConfig = config;\n        try {\n            InetAddress address = InetAddress.getByName(PROXY_HOST);\n            this.mServerSocket = new ServerSocket(0, 8, address);\n            this.mPort = mServerSocket.getLocalPort();\n            mConfig.setConfig(PROXY_HOST, mPort);\n            CountDownLatch startSignal = new CountDownLatch(1);\n            WaitSocketRequestsTask task = new WaitSocketRequestsTask(startSignal);\n            mRequestThread = new Thread(task);\n            mRequestThread.setName(\"VideoProxyCacheThread\");\n            mRequestThread.start();\n            startSignal.await();\n        } catch (Exception e) {\n            shutdown();\n            LogUtils.w(\"Cannot create serverSocket, exception=\" + e);\n        }\n\n    }\n\n    private class WaitSocketRequestsTask implements Runnable {\n\n        private CountDownLatch mLatch;\n\n        public WaitSocketRequestsTask(CountDownLatch latch) { mLatch = latch; }\n\n        @Override\n        public void run() {\n            mLatch.countDown();\n            initSocketProcessor();\n        }\n    }\n\n    private void initSocketProcessor() {\n        do {\n            try {\n                Socket socket = mServerSocket.accept();\n                if (mConfig.getConnTimeOut() > 0)\n                    socket.setSoTimeout(mConfig.getConnTimeOut());\n                mSocketPool.submit(new SocketProcessorTask(socket, mConfig));\n            } catch (Exception e) {\n                LogUtils.w(\n                        \"WaitRequestsRun ServerSocket accept failed, exception=\" + e);\n            }\n        } while (!mServerSocket.isClosed());\n    }\n\n    private void shutdown() {\n        if (mServerSocket != null) {\n            try {\n                mServerSocket.close();\n            } catch (Exception e) {\n                LogUtils.w( \"ServerSocket close failed, exception=\"+e);\n            }finally {\n                mSocketPool.shutdown();\n                if (mRequestThread != null && mRequestThread.isAlive()) {\n                    mRequestThread.interrupt();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/utils/DownloadExceptionUtils.java",
    "content": "package com.media.cache.utils;\n\nimport com.media.cache.VideoCacheException;\n\nimport java.io.FileNotFoundException;\nimport java.net.SocketTimeoutException;\nimport java.net.UnknownHostException;\n\npublic class DownloadExceptionUtils {\n\n    /**\n     * https://cdn7-video.hnqiyouquan.com:8081/20200223/XA0V108f/index.m3u8  下载慢\n     * http://hao.zuida-youku.com/20170618/lQl8AJpD/index.m3u8               SocketTimeoutException\n     * https://ae01.alicdn.com/kf/U04e25e8e7f7e46b7a7d98986ecf61205D.png?.m3u8  not M3U8?\n     */\n    private static final int UNKNOWN_ERROR = -1;\n    private static final int SOCKET_TIMEOUT_ERROR = 1000; //http://hao.zuida-youku.com/20170704/v3xK5MLu/index.m3u8\n    private static final int FILE_NOT_FOUND_ERROR = 1001; //https://hao.czybjz.com/20171023/GBGFCDHf/index.m3u8\n    private static final int UNKNOWN_HOST_ERROR = 1002;   //https://cn1.sw92.com/avid5e3b9a3acab6d/index.m3u8\n\n    //Custom Exception\n    private static final int FILE_LENGTH_FETCHED_ERROR = 2000; //http://cdn.videobanker.com/video.mp4?id=13297481&token=eafb2012f9d225b647fb36d556954e73&quality=480\n    private static final int M3U8_FILE_CONTENT_ERROR = 2001;   //http://video.dnsoy.com:8091/9720170813/RK3RLO562/550kb/hls/index.m3u8\n    private static final int MIMETYPE_NULL_ERROR = 2002;       //https://api.xiaomingming.org/cloud/h\n    private static final int MIMETYPE_NOT_FOUND = 2003;        //https://sina.com-h-sina.com/share/fb5ac34d9ac3cc3883230cb5b2b417bb\n\n    public static final String FILE_LENGTH_FETCHED_ERROR_STRING = \"File Length Cannot be fetched\";\n    public static final String M3U8_FILE_CONTENT_ERROR_STRING = \"M3U8 File content error\";\n    public static final String MIMETYPE_NULL_ERROR_STRING = \"MimeType is null\";\n    public static final String MIMETYPE_NOT_FOUND_STRING = \"MimeType not found\";\n\n    public static int getErrorCode(Throwable e) {\n        if (e instanceof SocketTimeoutException) {\n            return SOCKET_TIMEOUT_ERROR;\n        } else if (e instanceof FileNotFoundException) {\n            return FILE_NOT_FOUND_ERROR;\n        } else if (e instanceof VideoCacheException) {\n            if (((VideoCacheException) e).getMsg().equals(FILE_LENGTH_FETCHED_ERROR_STRING)) {\n                return FILE_LENGTH_FETCHED_ERROR;\n            } else if (((VideoCacheException) e).getMsg().equals(M3U8_FILE_CONTENT_ERROR_STRING)) {\n                return M3U8_FILE_CONTENT_ERROR;\n            } else if (((VideoCacheException) e).getMsg().equals(MIMETYPE_NULL_ERROR_STRING)) {\n                return MIMETYPE_NULL_ERROR;\n            } else if(((VideoCacheException) e).getMsg().equals(MIMETYPE_NOT_FOUND_STRING)) {\n\n            }\n        } else if (e instanceof UnknownHostException) {\n            return UNKNOWN_HOST_ERROR;\n        }\n        return UNKNOWN_ERROR;\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/utils/HttpUtils.java",
    "content": "package com.media.cache.utils;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.LocalProxyConfig;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.NoRouteToHostException;\nimport java.net.ProtocolException;\nimport java.net.URL;\nimport java.security.cert.X509Certificate;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.HttpsURLConnection;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLSession;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\n\npublic class HttpUtils {\n\n    public static int MAX_REDIRECT = 5;\n    public static final int RESPONSE_OK = 200;\n\n    public static boolean matchHttpSchema(String url) {\n        if (TextUtils.isEmpty(url))\n            return false;\n        Uri uri = Uri.parse(url);\n        String schema = uri.getScheme();\n        return \"http\".equals(schema) || \"https\".equals(schema);\n    }\n\n    public static String getMimeType(LocalProxyConfig config, String videoUrl, HashMap<String, String> headers) throws IOException {\n        String mimeType = null;\n        URL url = null;\n        try {\n            url = new URL(videoUrl);\n        } catch (MalformedURLException e) {\n            LogUtils.w(\"VideoUrl(\" + videoUrl +\") packages error, exception = \" + e.getMessage());\n            throw new MalformedURLException(\"URL parse error.\");\n        }\n        HttpURLConnection connection = null;\n        if (url != null) {\n            try {\n                connection = makeConnection(config, url, headers);\n            } catch (IOException e) {\n                LogUtils.w(\"Unable to connect videoUrl(\" + videoUrl + \"), exception = \" + e.getMessage());\n                closeConnection(connection);\n                throw new IOException(\"getMimeType connect failed.\");\n            }\n            int responseCode = 0;\n            if (connection != null) {\n                try {\n                    responseCode = connection.getResponseCode();\n                }catch (IOException e) {\n                    LogUtils.w(\"Unable to Get reponseCode videoUrl(\" + videoUrl + \"), exception = \" + e.getMessage());\n                    closeConnection(connection);\n                    throw new IOException(\"getMimeType get responseCode failed.\");\n                }\n                if (responseCode == RESPONSE_OK) {\n                    String contentType = connection.getContentType();\n                    LogUtils.i(\"contentType = \" + contentType);\n                    return contentType;\n                }\n\n            }\n        }\n        return mimeType;\n    }\n\n    public static String getFinalUrl(LocalProxyConfig config, String videoUrl, HashMap<String, String> headers) throws IOException {\n        URL url = null;\n        try {\n            url = new URL(videoUrl);\n        } catch (MalformedURLException e) {\n            LogUtils.w(\"VideoUrl(\" + videoUrl +\") packages error, exception = \" + e.getMessage());\n            throw new MalformedURLException(\"URL parse error.\");\n        }\n        url = handleRedirectRequest(config, url, headers);\n        return url.toString();\n    }\n\n    public static URL handleRedirectRequest(LocalProxyConfig config, URL url, HashMap<String, String> headers) throws IOException {\n        int redirectCount = 0;\n        while (redirectCount++ < MAX_REDIRECT) {\n            HttpURLConnection connection = makeConnection(config, url, headers);\n            int responseCode = connection.getResponseCode();\n            if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE\n                    || responseCode == HttpURLConnection.HTTP_MOVED_PERM\n                    || responseCode == HttpURLConnection.HTTP_MOVED_TEMP\n                    || responseCode == HttpURLConnection.HTTP_SEE_OTHER\n                    && (responseCode == 307 /* HTTP_TEMP_REDIRECT */\n                    || responseCode == 308 /* HTTP_PERM_REDIRECT */)) {\n                String location = connection.getHeaderField(\"Location\");\n                connection.disconnect();\n                url = handleRedirect(url, location);\n                return handleRedirectRequest(config, url, headers);\n            } else {\n                return url;\n            }\n        }\n        throw new NoRouteToHostException(\"Too many redirects: \" + redirectCount);\n    }\n\n    private static HttpURLConnection makeConnection(LocalProxyConfig config, URL url, HashMap<String, String> headers) throws IOException {\n        HttpURLConnection connection = null;\n        connection = (HttpURLConnection)url.openConnection();\n        if (config.shouldIgnoreAllCertErrors() && connection instanceof HttpsURLConnection) {\n            trustAllCert((HttpsURLConnection)connection);\n        }\n        connection.setConnectTimeout(config.getConnTimeOut());\n        connection.setReadTimeout(config.getReadTimeOut());\n        if (headers != null) {\n            for (Map.Entry<String, String> item : headers.entrySet()) {\n                connection.setRequestProperty(item.getKey(), item.getValue());\n            }\n        }\n        connection.connect();\n        return connection;\n    }\n\n    private static URL handleRedirect(URL originalUrl, String location) throws IOException {\n        if (location == null) {\n            throw new ProtocolException(\"Null location redirect\");\n        }\n        URL url = new URL(originalUrl, location);\n        String protocol = url.getProtocol();\n        if (!\"https\".equals(protocol) && !\"http\".equals(protocol)) {\n            throw new ProtocolException(\"Unsupported protocol redirect: \" + protocol);\n        }\n        return url;\n    }\n\n    private static void closeConnection(HttpURLConnection connection) {\n        if (connection != null) {\n            connection.disconnect();\n            connection = null;\n        }\n    }\n\n    public static void trustAllCert(HttpsURLConnection httpsURLConnection) {\n        SSLContext sslContext = null;\n        try {\n            sslContext = SSLContext.getInstance(\"TLS\");\n            if (sslContext != null) {\n                TrustManager tm = new X509TrustManager() {\n                    public X509Certificate[] getAcceptedIssuers() {\n                        return null;\n                    }\n\n                    public void checkClientTrusted(X509Certificate[] chain, String authType) {\n//                        LogUtils.i( \"checkClientTrusted.\");\n                    }\n\n                    public void checkServerTrusted(X509Certificate[] chain, String authType) {\n//                        LogUtils.i(\"checkServerTrusted.\");\n                    }\n                };\n                sslContext.init(null, new TrustManager[] { tm }, null);\n            }\n        } catch (Exception e) {\n            LogUtils.w( \"SSLContext init failed\");\n        }\n        // Cannot do ssl checkl.\n        if (sslContext != null) {\n            httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());\n        }\n        //Trust the cert.\n        HostnameVerifier hostnameVerifier = new HostnameVerifier() {\n            @Override\n            public boolean verify(String hostname, SSLSession session) {\n                return true;\n            }\n        };\n        httpsURLConnection.setHostnameVerifier(hostnameVerifier);\n    }\n}\n\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/utils/LocalProxyThreadUtils.java",
    "content": "package com.media.cache.utils;\n\nimport android.os.Process;\n\nimport com.android.baselib.utils.LogUtils;\n\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\npublic class LocalProxyThreadUtils {\n\n    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();\n    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;\n    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;\n    private static final int KEEP_ALIVE = 1;\n    private static final int QUEUE_SIZE = 2 ^ CPU_COUNT;\n\n    private static class MediaWorkerThreadFactory implements ThreadFactory {\n        public Thread newThread(Runnable r) {\n            return new MediaWorkerThread(r);\n        }\n    }\n\n    private static class MediaWorkerThread extends Thread {\n        public MediaWorkerThread(Runnable r) {\n            super(r, \"vivo_media_worker_pool_thread\");\n        }\n\n        @Override\n        public void run() {\n            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);\n            long startTime = System.currentTimeMillis();\n            super.run();\n            long endTime = System.currentTimeMillis();\n            LogUtils.i(\"ProxyCacheThreadHandler execution time: \" + (endTime - startTime));\n        }\n    }\n\n    private static final BlockingQueue<Runnable> sThreadPoolWorkQueue =\n            new LinkedBlockingQueue<>(QUEUE_SIZE);\n    private static final ExecutorService sThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,\n            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sThreadPoolWorkQueue,\n            new MediaWorkerThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());\n\n    public static Future submitCallbackTask(Callable task) {\n        return sThreadPoolExecutor.submit(task);\n    }\n\n    public static Future submitRunnableTask(Runnable task) {\n        return sThreadPoolExecutor.submit(task);\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/utils/LocalProxyUtils.java",
    "content": "package com.media.cache.utils;\n\nimport android.text.TextUtils;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.media.cache.model.VideoCacheInfo;\n\nimport java.io.BufferedReader;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.security.MessageDigest;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class LocalProxyUtils {\n\n    public static final int UPDATE_INTERVAL = 1000;\n    public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;\n    private static final Pattern URL_PATTERN = Pattern.compile(\"GET /(.*) HTTP\");\n    public static final String INFO_FILE = \"video.info\";\n    public static final String SPLIT_STR = \"&jeffmony&\";\n\n    public static boolean isFloatEqual(float f1, float f2) {\n        if (Math.abs(f1-f2) < 0.0001f) {\n            return true;\n        }\n        return false;\n    }\n\n    public static void close(Closeable closeable) {\n        if (closeable != null){\n            try {\n                closeable.close();\n            } catch (Exception e) {\n                LogUtils.w(\"LocalProxyUtils close \" + closeable +\" failed, exception = \" +e);\n            }\n        }\n    }\n\n    public static String findUrlForStream(InputStream inputStream) throws Exception {\n        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream,\"UTF-8\"));\n        StringBuilder builder = new StringBuilder();\n        String line;\n        while (!TextUtils.isEmpty(line = reader.readLine())){\n            builder.append(line)\n                    .append(\"\\n\");\n        }\n        Matcher matcher = URL_PATTERN.matcher(builder.toString());\n        if (matcher.find()) {\n            return matcher.group(1);\n        }\n        throw new Exception(\"Url not found\");\n    }\n\n    public static String encodeUri(String str) {\n        try {\n            return URLEncoder.encode(str, \"UTF-8\");\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error encoding url\", e);\n        }\n    }\n\n\n    public static String decodeUri(String str) {\n        String decoded = null;\n        try {\n            decoded = URLDecoder.decode(str, \"UTF-8\");\n        } catch (Exception ignored) {\n            LogUtils.w(\"Encoding not supported, ignored: \"+ignored.getMessage());\n        }\n        return decoded;\n    }\n\n    private static String bytesToHexString(byte[] bytes) {\n        StringBuffer sb = new StringBuffer();\n        for (byte b : bytes) {\n            sb.append(String.format(\"%02x\", b));\n        }\n        return sb.toString();\n    }\n\n    public static String computeMD5(String string) {\n        try {\n            MessageDigest messageDigest = MessageDigest.getInstance(\"MD5\");\n            byte[] digestBytes = messageDigest.digest(string.getBytes());\n            return bytesToHexString(digestBytes);\n        } catch (Exception e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    private static Object sFileLock = new Object();\n\n    public static VideoCacheInfo readProxyCacheInfo(File dir) {\n        File file = new File(dir, INFO_FILE);\n        if (!file.exists()) {\n            LogUtils.i(\"readProxyCacheInfo failed, file not exist.\");\n            return null;\n        }\n        ObjectInputStream fis = null;\n        try {\n            synchronized (sFileLock) {\n                fis = new ObjectInputStream(new FileInputStream(file));\n                VideoCacheInfo info = (VideoCacheInfo) fis.readObject();\n                return info;\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"readProxyCacheInfo failed, exception=\"+e.getMessage());\n        } finally {\n            try {\n                if (fis != null) {\n                    fis.close();\n                }\n            } catch (Exception e) {\n                LogUtils.w(\"readProxyCacheInfo failed, close fis failed.\");\n            }\n        }\n        return null;\n    }\n\n    public static void writeProxyCacheInfo(VideoCacheInfo info, File dir) {\n        File file = new File(dir, INFO_FILE);\n        ObjectOutputStream fos = null;\n        try {\n            synchronized (sFileLock) {\n                fos = new ObjectOutputStream(new FileOutputStream(file));\n                fos.writeObject(info);\n            }\n        } catch (Exception e) {\n            LogUtils.w(\"writeProxyCacheInfo failed, exception=\"+e.getMessage());\n        } finally {\n            try {\n                if (fos != null) {\n                    fos.close();\n                }\n            } catch (Exception e) {\n                LogUtils.w(\"writeProxyCacheInfo failed, close fos failed.\");\n            }\n        }\n    }\n\n    public static void setLastModifiedNow(File file) throws IOException {\n        if (file.exists()) {\n            long now = System.currentTimeMillis();\n            boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work\n            if (!modified) {\n                modify(file);\n            }\n        }\n    }\n\n    private static void modify(File dir) throws IOException {\n        File tempFile = new File(dir, \"tempFile\");\n        if (!tempFile.exists()) {\n            tempFile.createNewFile();\n            tempFile.delete();\n        } else {\n            tempFile.delete();\n        }\n    }\n\n    public static boolean isChinese(char c) {\n        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);\n        if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS\n                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS\n                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A\n                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION\n                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION\n                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {\n            return true;\n        }\n        return false;\n    }\n\n    public static boolean isMessyCode(String strName) {\n        Pattern p = Pattern.compile(\"\\\\s*|t*|r*|n*\");\n        Matcher m = p.matcher(strName);\n        String after = m.replaceAll(\"\");\n        String temp = after.replaceAll(\"\\\\p{P}\", \"\");\n        char[] ch = temp.trim().toCharArray();\n        float chLength = ch.length;\n        float count = 0;\n        for (int i = 0; i < ch.length; i++) {\n            char c = ch[i];\n            if (!Character.isLetterOrDigit(c)) {\n                if (!isChinese(c)) {\n                    count = count + 1;\n                }\n            }\n        }\n        if (chLength <= 0)\n            return false;\n        float result = count / chLength;\n        if (result > 0.4) {\n            return true;\n        } else {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "mediaproxy/src/main/java/com/media/cache/utils/StorageUtils.java",
    "content": "package com.media.cache.utils;\n\nimport android.content.Context;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic class StorageUtils {\n\n    public static File getVideoCacheDir(Context context) {\n        return new File(context.getExternalCacheDir(), \".local\");\n    }\n\n    public static void clearVideoCacheDir(Context context) throws IOException {\n        File videoCacheDir = getVideoCacheDir(context);\n        cleanDirectory(videoCacheDir);\n    }\n\n    private static void cleanDirectory(File file) throws IOException {\n        if (!file.exists()) {\n            return;\n        }\n        File[] contentFiles = file.listFiles();\n        if (contentFiles != null) {\n            for (File contentFile : contentFiles) {\n                delete(contentFile);\n            }\n        }\n    }\n\n    public static void delete(File file)throws IOException {\n        if (file.isFile() && file.exists()) {\n            deleteOrThrow(file);\n        } else {\n            cleanDirectory(file);\n            deleteOrThrow(file);\n        }\n    }\n\n    private static void deleteOrThrow(File file) throws IOException {\n        if (file.exists()) {\n            boolean isDeleted = file.delete();\n            if (!isDeleted) {\n                throw new IOException(\n                        String.format(\"File %s can't be deleted\", file.getAbsolutePath()));\n            }\n        }\n    }\n\n    public static List<File> getLruFileList(File dir) {\n        List<File> result = new LinkedList<>();\n        File[] files = dir.listFiles();\n        if (files != null) {\n            result = Arrays.asList(files);\n            Collections.sort(result, new LastModifiedComparator());\n        }\n        return result;\n    }\n\n    private static final class LastModifiedComparator implements Comparator<File> {\n\n        @Override\n        public int compare(File lhs, File rhs) {\n            return compareLong(lhs.lastModified(), rhs.lastModified());\n        }\n\n        private int compareLong(long first, long second) {\n            return (first < second) ? -1 : ((first == second) ? 0 : 1);\n        }\n    }\n\n    public static boolean deleteFile(File file) {\n        if (file.isDirectory()) {\n            for (File f : file.listFiles()) {\n                if (!f.delete())\n                    return false;\n            }\n            return file.delete();\n        } else {\n            return file.delete();\n        }\n    }\n\n    public static void deleteCacheFile(File file) {\n        if (!file.exists()) {\n            return;\n        }\n        if (file.isDirectory()) {\n            File[] listFiles = file.listFiles();\n            for (File f : listFiles) {\n                if (f.isDirectory()) {\n                    deleteCacheFile(f);\n                    f.delete();\n                } else {\n                    f.delete();\n                }\n            }\n            file.delete();\n        } else {\n            file.delete();\n        }\n    }\n\n    public static long countTotalSize(List<File> files) {\n        long totalSize = 0;\n        for (File file : files)  {\n            totalSize += countTotalSize(file);\n        }\n        return totalSize;\n    }\n\n    public static long countTotalSize(File file) {\n        if (file.isDirectory()) {\n            long totalSize = 0;\n            for (File f : file.listFiles()) {\n                totalSize += f.length();\n            }\n            return totalSize;\n        } else {\n            return file.length();\n        }\n    }\n\n}\n"
  },
  {
    "path": "mediaproxy/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MediaProxyLib</string>\n</resources>\n"
  },
  {
    "path": "playersdk/.gitignore",
    "content": "/build\n/playersdk.iml\n"
  },
  {
    "path": "playersdk/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 27\n    buildToolsVersion \"27.0.2\"\n\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 27\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation project(path: ':base')\n    implementation project(path: ':mediaproxy')\n    implementation project(path: ':exoplayer')\n    implementation project(path: ':ijkplayer')\n}\n"
  },
  {
    "path": "playersdk/playersdk.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module external.linked.project.id=\":playersdk\" external.linked.project.path=\"$MODULE_DIR$\" external.root.project.path=\"$MODULE_DIR$/..\" external.system.id=\"GRADLE\" type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <facet type=\"android-gradle\" name=\"Android-Gradle\">\n      <configuration>\n        <option name=\"GRADLE_PROJECT_PATH\" value=\":playersdk\" />\n        <option name=\"LAST_SUCCESSFUL_SYNC_AGP_VERSION\" value=\"3.5.3\" />\n        <option name=\"LAST_KNOWN_AGP_VERSION\" value=\"3.5.3\" />\n      </configuration>\n    </facet>\n    <facet type=\"android\" name=\"Android\">\n      <configuration>\n        <option name=\"SELECTED_BUILD_VARIANT\" value=\"debug\" />\n        <option name=\"ASSEMBLE_TASK_NAME\" value=\"assembleDebug\" />\n        <option name=\"COMPILE_JAVA_TASK_NAME\" value=\"compileDebugSources\" />\n        <afterSyncTasks>\n          <task>generateDebugSources</task>\n        </afterSyncTasks>\n        <option name=\"ALLOW_USER_CONFIGURATION\" value=\"false\" />\n        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\"/src/main/AndroidManifest.xml\" />\n        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/src/main/res\" />\n        <option name=\"RES_FOLDERS_RELATIVE_PATH\" value=\"file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug\" />\n        <option name=\"TEST_RES_FOLDERS_RELATIVE_PATH\" value=\"\" />\n        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/src/main/assets\" />\n        <option name=\"PROJECT_TYPE\" value=\"1\" />\n      </configuration>\n    </facet>\n  </component>\n  <component name=\"NewModuleRootManager\" LANGUAGE_LEVEL=\"JDK_1_8\">\n    <output url=\"file://$MODULE_DIR$/build/intermediates/javac/debug/classes\" />\n    <output-test url=\"file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes\" />\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/debug\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out\" isTestSource=\"false\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/debug\" type=\"java-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug\" type=\"java-test-resource\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out\" isTestSource=\"true\" generated=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/debug/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTestDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/testDebug/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/res\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/resources\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/assets\" type=\"java-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/aidl\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/java\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/rs\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/main/shaders\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/androidTest/shaders\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/res\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/resources\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/assets\" type=\"java-test-resource\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/aidl\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/java\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/rs\" isTestSource=\"true\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/src/test/shaders\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"jdk\" jdkName=\"Android API 27 Platform\" jdkType=\"Android SDK\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"module\" module-name=\"base\" />\n    <orderEntry type=\"module\" module-name=\"mediaproxy\" />\n    <orderEntry type=\"module\" module-name=\"exoplayer\" />\n    <orderEntry type=\"module\" module-name=\"ijkplayer\" />\n  </component>\n</module>"
  },
  {
    "path": "playersdk/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "playersdk/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.android.player\" />\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/CommonPlayer.java",
    "content": "package com.android.player;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.view.Surface;\n\nimport com.android.player.impl.ExoPlayerImpl;\nimport com.android.player.impl.IjkPlayerImpl;\nimport com.android.player.impl.MediaPlayerImpl;\nimport com.android.player.impl.PlayerImpl;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class CommonPlayer implements IPlayer {\n\n    private PlayerImpl mPlayerImpl;\n    private PlayerType mType;\n\n    public CommonPlayer(Context context) {\n        this(context, PlayerType.EXO_PLAYER);\n    }\n\n    public CommonPlayer(Context context, PlayerType type) {\n        this(context, type, null);\n    }\n\n    public CommonPlayer(Context context, PlayerType type, PlayerAttributes attributes) {\n        this.mType = type;\n\n        if (type == PlayerType.MEDIA_PLAYER) {\n            mPlayerImpl = new MediaPlayerImpl(context, attributes);\n        } else if (type == PlayerType.EXO_PLAYER) {\n            mPlayerImpl = new ExoPlayerImpl(context, attributes);\n        } else if (type == PlayerType.IJK_PLAYER) {\n            mPlayerImpl = new IjkPlayerImpl(context, attributes);\n        }\n    }\n\n    @Override\n    public void startLocalProxy(String url) {\n        mPlayerImpl.startLocalProxy(url);\n    }\n\n    @Override\n    public void setOriginUrl(String url) {\n        mPlayerImpl.setOriginUrl(url);\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayerImpl.setDataSource(path);\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException {\n        mPlayerImpl.setDataSource(fd);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayerImpl.setDataSource(context, uri);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayerImpl.setDataSource(context, uri, headers);\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayerImpl.setDataSource(fd, offset, length);\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n        mPlayerImpl.setSurface(surface);\n    }\n\n    @Override\n    public void setOnPreparedListener(OnPreparedListener listener) {\n        mPlayerImpl.setOnPreparedListener(listener);\n    }\n\n    @Override\n    public void setOnVideoSizeChangedListener(OnVideoSizeChangedListener listener) {\n        mPlayerImpl.setOnVideoSizeChangedListener(listener);\n    }\n\n    @Override\n    public void setOnErrorListener(OnErrorListener listener) {\n        mPlayerImpl.setOnErrorListener(listener);\n    }\n\n    @Override\n    public void setOnLocalProxyCacheListener(OnLocalProxyCacheListener listener) {\n        mPlayerImpl.setOnLocalProxyCacheListener(listener);\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        mPlayerImpl.prepareAsync();\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mPlayerImpl.start();\n    }\n\n    @Override\n    public void openPlay(PlayerAttributes attributes) {\n        mPlayerImpl.openPlay(attributes);\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mPlayerImpl.pause();\n    }\n\n    @Override\n    public void setSpeed(float speed) {\n        mPlayerImpl.setSpeed(speed);\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mPlayerImpl.stop();\n    }\n\n    @Override\n    public void release() {\n        mPlayerImpl.release();\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        mPlayerImpl.seekTo(msec);\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return mPlayerImpl.getCurrentPosition();\n    }\n\n    @Override\n    public long getDuration() {\n        return mPlayerImpl.getDuration();\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mPlayerImpl.isPlaying();\n    }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/IPlayer.java",
    "content": "package com.android.player;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.view.Surface;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\npublic interface IPlayer {\n\n    void startLocalProxy(String url);\n\n    void setDataSource(Context context, Uri uri)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    void setDataSource(String path)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    void setDataSource(Context context, Uri uri, Map<String, String> headers)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    void setDataSource(FileDescriptor fd)\n            throws IOException, IllegalArgumentException, IllegalStateException;\n\n    void setDataSource(FileDescriptor fd, long offset, long length)\n            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;\n\n    void setSurface(Surface surface);\n\n    void prepareAsync() throws IllegalStateException;\n\n    void start() throws IllegalStateException;\n\n    void openPlay(PlayerAttributes attributes);\n\n    void stop() throws IllegalStateException;\n\n    void pause() throws IllegalStateException;\n\n    void setSpeed(float speed);\n\n    void release();\n\n    void seekTo(long msec) throws IllegalStateException;\n\n    long getCurrentPosition();\n\n    long getDuration();\n\n    boolean isPlaying();\n\n    void setOnPreparedListener(OnPreparedListener listener);\n\n    void setOnVideoSizeChangedListener(\n            OnVideoSizeChangedListener listener);\n\n    void setOnErrorListener(OnErrorListener listener);\n\n    void setOriginUrl(String url);\n    void setOnLocalProxyCacheListener(OnLocalProxyCacheListener listener);\n\n    interface OnPreparedListener {\n        void onPrepared(IPlayer mp);\n    }\n\n    interface OnVideoSizeChangedListener {\n        void onVideoSizeChanged(IPlayer mp, int width, int height,\n                                int rotationDegree,\n                                float pixelRatio,\n                                float darRatio);\n    }\n\n    interface OnErrorListener {\n        void onError(IPlayer mp, int what, String msg);\n    }\n\n    interface OnLocalProxyCacheListener {\n        void onCacheReady(IPlayer mp, String proxyUrl);\n        void onCacheProgressChanged(IPlayer mp, int percent, long cachedSize);\n        void onCacheSpeedChanged(IPlayer mp, float speed);\n        void onCacheForbidden(IPlayer mp, String url);\n        void onCacheFinished(IPlayer mp);\n    }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/PlayerAttributes.java",
    "content": "package com.android.player;\n\npublic class PlayerAttributes {\n\n    private boolean mVideoCacheSwitch;\n    private String mVideoUrl;\n    private int mTaskMode;\n\n    public PlayerAttributes(String url) {\n        mVideoUrl = url;\n    }\n\n    public void setVideoCacheSwitch(boolean videoCacheSwitch) {\n        this.mVideoCacheSwitch = videoCacheSwitch;\n    }\n\n    public boolean videoCacheSwitch() {\n        return mVideoCacheSwitch;\n    }\n\n    public void setTaskMode(int mode) {\n        mTaskMode = mode;\n    }\n\n    public int getTaskMode() {\n        return mTaskMode;\n    }\n\n    public String getVideoUrl() { return mVideoUrl; }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/PlayerType.java",
    "content": "package com.android.player;\n\npublic enum PlayerType {\n    MEDIA_PLAYER,\n    EXO_PLAYER,\n    IJK_PLAYER,\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/impl/ExoPlayerImpl.java",
    "content": "package com.android.player.impl;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.view.Surface;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.android.player.PlayerAttributes;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.dash.DashMediaSource;\nimport com.google.android.exoplayer2.source.hls.HlsMediaSource;\nimport com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoListener;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\npublic class ExoPlayerImpl extends PlayerImpl {\n\n    private static final int PREPARE_NULL = 0x0;\n    private static final int PREPARING_STATE = 0x1;\n    private static final int PREPARED_STATE = 0x2;\n\n    private Context mContext;\n    private SimpleExoPlayer mPlayer;\n    private MediaSource mMediaSource;\n    private int mPrepareState = PREPARE_NULL;\n\n    private boolean mIsInitPlayerListener = false;\n    private PlayerEventListener mEventListener;\n    private PlayerVideoListener mVideoListener;\n\n    public ExoPlayerImpl(Context context, PlayerAttributes attributes) {\n        super(context, attributes);\n        mContext = context.getApplicationContext();\n        mPlayer = new SimpleExoPlayer.Builder(context).build();\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        super.setDataSource(path);\n        Uri uri = Uri.parse(path);\n        mMediaSource = createMediaSource(uri, null);\n        mUrl = uri.toString();\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException {\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        super.setDataSource(context, uri);\n        mMediaSource = createMediaSource(uri, null);\n        mUrl = uri.toString();\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        super.setDataSource(context, uri, headers);\n        mMediaSource = createMediaSource(uri, null);\n        mUrl = uri.toString();\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n        mPlayer.setVideoSurface(surface);\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        super.prepareAsync();\n        if (!mIsInitPlayerListener) {\n            initPlayerListener();\n        }\n        mPrepareState = PREPARING_STATE;\n        mPlayer.prepare(mMediaSource);\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mPlayer.setPlayWhenReady(true);\n        super.start();\n    }\n\n    @Override\n    public void doOpenPlay(String url) {\n        Uri uri = Uri.parse(url);\n        mPlayer.setPlayWhenReady(true);\n        mMediaSource = createMediaSource(uri, null);\n        prepareAsync();\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mPlayer.setPlayWhenReady(false);\n        super.pause();\n    }\n\n    @Override\n    public void setSpeed(float speed) {\n        PlaybackParameters parameters = new PlaybackParameters(speed);\n        mPlayer.setPlaybackParameters(parameters);\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mPlayer.stop();\n        super.stop();\n    }\n\n    @Override\n    public void release() {\n        mPlayer.removeVideoListener(mVideoListener);\n        mPlayer.removeListener(mEventListener);\n        mPlayer.release();\n        super.release();\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return mPlayer.getCurrentPosition();\n    }\n\n    @Override\n    public long getDuration() {\n        return mPlayer.getDuration();\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mPlayer.isPlaying();\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        mPlayer.seekTo(msec);\n        super.seekTo(msec);\n    }\n\n    private void initPlayerListener() {\n        mEventListener = new PlayerEventListener();\n        mVideoListener = new PlayerVideoListener();\n        mPlayer.addListener(mEventListener);\n        mPlayer.addVideoListener(mVideoListener);\n        mIsInitPlayerListener = true;\n    }\n\n    private DataSource.Factory buildDataSourceFactory() {\n        String userAgent = Util.getUserAgent(mContext, \"ExoPlayerDemo\");\n        DefaultDataSourceFactory upstreamFactory =\n                new DefaultDataSourceFactory(mContext, new DefaultHttpDataSourceFactory(userAgent));\n        return upstreamFactory;\n    }\n\n    private MediaSource createMediaSource(Uri uri, String extension) {\n        int type = Util.inferContentType(uri, extension);\n        DataSource.Factory dataSourceFactory = buildDataSourceFactory();\n        switch (type) {\n            case C.TYPE_DASH:\n                return new DashMediaSource.Factory(dataSourceFactory)\n                        .createMediaSource(uri);\n            case C.TYPE_SS:\n                return new SsMediaSource.Factory(dataSourceFactory)\n                        .createMediaSource(uri);\n            case C.TYPE_HLS:\n                return new HlsMediaSource.Factory(dataSourceFactory)\n                        .createMediaSource(uri);\n            case C.TYPE_OTHER:\n                return new ProgressiveMediaSource.Factory(dataSourceFactory)\n                        .createMediaSource(uri);\n            default:\n                throw new IllegalStateException(\"Unsupported type: \" + type);\n        }\n    }\n\n    private class PlayerEventListener implements Player.EventListener {\n\n        @Override\n        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            LogUtils.d(\"onPlayerStateChanged playWhenReady=\"+playWhenReady+\", playbackState=\"+playbackState);\n            switch(playbackState) {\n                case Player.STATE_BUFFERING:\n                    break;\n                case Player.STATE_IDLE:\n                    break;\n                case Player.STATE_READY:\n                    if (mPrepareState == PREPARING_STATE) {\n                        notifyOnPrepared();\n                        mPrepareState = PREPARED_STATE;\n                    }\n                    break;\n                case Player.STATE_ENDED:\n                    break;\n                default:\n                    break;\n            }\n        }\n\n        @Override\n        public void onPlayerError(ExoPlaybackException error) {\n            notifyOnError(error.type, error.getCause().getMessage());\n        }\n\n        @Override\n        public void onIsPlayingChanged(boolean isPlaying) {\n            LogUtils.d(\"onIsPlayingChanged isPlaying=\"+isPlaying);\n        }\n    }\n\n    private class PlayerVideoListener implements VideoListener {\n\n        @Override\n        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n            notifyOnVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio, 1.0f);\n        }\n\n        @Override\n        public void onRenderedFirstFrame() {\n\n        }\n\n        @Override\n        public void onSurfaceSizeChanged(int width, int height) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/impl/IjkPlayerImpl.java",
    "content": "package com.android.player.impl;\n\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.net.Uri;\nimport android.view.Surface;\n\nimport com.android.player.PlayerAttributes;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\nimport tv.danmaku.ijk.media.player.IMediaPlayer;\nimport tv.danmaku.ijk.media.player.IjkMediaPlayer;\n\npublic class IjkPlayerImpl extends PlayerImpl {\n\n    private IjkMediaPlayer mPlayer;\n\n    public IjkPlayerImpl(Context context, PlayerAttributes attributes) {\n        super(context, attributes);\n\n        mPlayer = new IjkMediaPlayer();\n\n        //不用MediaCodec编解码\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,\n            \"mediacodec\", 1);\n\n        //不用opensles编解码\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,\n            \"opensles\", 0);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,\n            \"overlay-format\", IjkMediaPlayer.SDL_FCC_RV32);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,\n                \"framedrop\", 1);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,\n            \"start-on-prepared\", 0);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT,\n            \"http-detect-range-support\", 0);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT,\n            \"timeout\", 10000000);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT,\n            \"reconnect\", 1);\n        mPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC,\n            \"skip_loop_filter\", 48);\n        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);\n\n        initPlayerListeners();\n    }\n\n    private void initPlayerListeners() {\n        mPlayer.setOnPreparedListener(mOnPreparedListener);\n        mPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);\n        mPlayer.setOnErrorListener(mOnErrorListener);\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayer.setDataSource(path);\n        mUrl = path;\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException {\n        mPlayer.setDataSource(fd);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayer.setDataSource(context, uri);\n        mUrl = uri.toString();\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayer.setDataSource(context, uri, headers);\n        mUrl = uri.toString();\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n        mPlayer.setSurface(surface);\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        super.prepareAsync();\n        mPlayer.prepareAsync();\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mPlayer.start();\n        super.start();\n    }\n\n    @Override\n    public void doOpenPlay(String url) {\n\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mPlayer.isPlaying();\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mPlayer.pause();\n        super.pause();\n    }\n\n    @Override\n    public void setSpeed(float speed) {\n        mPlayer.setSpeed(speed);\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        mPlayer.seekTo(msec);\n        super.seekTo(msec);\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mPlayer.stop();\n        super.stop();\n    }\n\n    @Override\n    public void release() {\n        mPlayer.release();\n        super.release();\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return mPlayer.getCurrentPosition();\n    }\n\n    @Override\n    public long getDuration() {\n        return mPlayer.getDuration();\n    }\n\n    private IjkMediaPlayer.OnPreparedListener mOnPreparedListener = new IjkMediaPlayer.OnPreparedListener() {\n\n        @Override\n        public void onPrepared(IMediaPlayer mp) {\n            notifyOnPrepared();\n        }\n\n    };\n\n    private IjkMediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new IjkMediaPlayer.OnVideoSizeChangedListener() {\n\n        @Override\n        public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sar_num, int sar_den) {\n            notifyOnVideoSizeChanged(width, height, sar_num, sar_den, 0);\n        }\n\n    };\n\n    private IjkMediaPlayer.OnVideoDarSizeChangedListener mOnVideoDarSizeChangedListener = new IjkMediaPlayer.OnVideoDarSizeChangedListener() {\n        @Override\n        public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sar_num, int sar_den, int dar_num, int dar_den) {\n            float pixelRatio = sar_num * 1.0f / sar_den;\n            if (Float.compare(pixelRatio, Float.NaN) == 0) {\n                pixelRatio = 1.0f;\n            }\n            float darRatio = dar_num * 1.0f / dar_den;\n            notifyOnVideoSizeChanged(width, height, 0, pixelRatio, darRatio);\n        }\n    };\n\n    private IjkMediaPlayer.OnErrorListener mOnErrorListener = new IjkMediaPlayer.OnErrorListener() {\n        @Override\n        public boolean onError(IMediaPlayer mp, int what, int extra) {\n            notifyOnError(what, \"\" + extra);\n            return true;\n        }\n    };\n\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/impl/MediaPlayerImpl.java",
    "content": "package com.android.player.impl;\n\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\nimport android.media.PlaybackParams;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.text.TextUtils;\nimport android.view.Surface;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.android.player.PlayerAttributes;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.Map;\n\npublic class MediaPlayerImpl extends PlayerImpl\n        implements\n        MediaPlayer.OnPreparedListener,\n        MediaPlayer.OnBufferingUpdateListener,\n        MediaPlayer.OnErrorListener,\n        MediaPlayer.OnInfoListener,\n        MediaPlayer.OnVideoSizeChangedListener,\n        MediaPlayer.OnSeekCompleteListener,\n        MediaPlayer.OnCompletionListener {\n\n    private Context mContext;\n    private MediaPlayer mPlayer = null;\n\n    private boolean mIsReleased = false;\n\n    private static byte[] LOCK = new byte[0];\n    private final Object mLock = new Object();\n\n    public MediaPlayerImpl(Context context, PlayerAttributes attributes) {\n        super(context, attributes);\n        synchronized (LOCK) {\n            mPlayer = new MediaPlayer();\n        }\n        mContext = context.getApplicationContext();\n        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);\n\n        mPlayer.setOnCompletionListener(this);\n        mPlayer.setOnErrorListener(this);\n        mPlayer.setOnInfoListener(this);\n        mPlayer.setOnPreparedListener(this);\n        mPlayer.setOnSeekCompleteListener(this);\n        mPlayer.setOnVideoSizeChangedListener(this);\n        mPlayer.setOnBufferingUpdateListener(this);\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {Uri uri = Uri.parse(path);\n        mUrl = uri.getPath();\n        String scheme = uri.getScheme();\n        if (!TextUtils.isEmpty(scheme) && scheme.equalsIgnoreCase(\"file\")) {\n            mPlayer.setDataSource(uri.getPath());\n        } else {\n            mPlayer.setDataSource(path);\n        }\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mPlayer.setDataSource(fd, offset, length);\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException {\n        mPlayer.setDataSource(fd);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mUrl = uri.getPath();\n        mPlayer.setDataSource(context, uri);\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n        mUrl = uri.getPath();\n        mPlayer.setDataSource(context, uri, headers);\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n        if (!mIsReleased) {\n            try {\n                mPlayer.setSurface(surface);\n            } catch (Exception e) {\n                LogUtils.e(\"setSurface failed, exception = \" + e.getMessage());\n            }\n        }\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n        super.prepareAsync();\n        mPlayer.prepareAsync();\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        mPlayer.start();\n    }\n\n    @Override\n    public void doOpenPlay(String url) {\n\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mPlayer.pause();\n    }\n\n    @Override\n    public void setSpeed(float speed) {\n        if (Build.VERSION.SDK_INT > 23) {\n            PlaybackParams params = new PlaybackParams();\n            params.setSpeed(speed);\n            mPlayer.setPlaybackParams(params);\n        } else {\n            LogUtils.w(\"setSpeed is invalid.\");\n        }\n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n        mPlayer.stop();\n    }\n\n    @Override\n    public void release() {\n        mPlayer.release();\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        int position = (int)msec;\n        mPlayer.seekTo(position);\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return mPlayer.getCurrentPosition();\n    }\n\n    @Override\n    public long getDuration() {\n        return mPlayer.getDuration();\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mPlayer.isPlaying();\n    }\n\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        notifyOnPrepared();\n    }\n\n    @Override\n    public void onBufferingUpdate(MediaPlayer mp, int percent) {\n\n    }\n\n    @Override\n    public boolean onError(MediaPlayer mp, int what, int extra) {\n        notifyOnError(what, \"\" + extra);\n        return true;\n    }\n\n    @Override\n    public boolean onInfo(MediaPlayer mp, int what, int extra) {\n        return false;\n    }\n\n    @Override\n    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n        notifyOnVideoSizeChanged(width, height, 0, 1, 1.0f);\n    }\n\n    @Override\n    public void onSeekComplete(MediaPlayer mp) {\n\n    }\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n\n    }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/impl/PlayerImpl.java",
    "content": "package com.android.player.impl;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.text.TextUtils;\nimport android.view.Surface;\n\nimport com.android.baselib.WeakHandler;\nimport com.android.player.IPlayer;\nimport com.android.player.PlayerAttributes;\nimport com.android.player.proxy.LocalProxyPlayerImpl;\n\nimport java.io.FileDescriptor;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic abstract class PlayerImpl implements IPlayer {\n\n    private OnPreparedListener mOnPreparedListener;\n    private OnVideoSizeChangedListener mOnVideoSizeChangedListener;\n    private OnErrorListener mOnErrorListener;\n    private OnLocalProxyCacheListener mOnLocalProxyCacheListener;\n\n    protected LocalProxyPlayerImpl mLocalProxyPlayerImpl;\n\n    protected String mUrl;\n\n    protected String mOriginUrl;\n\n    //Player settings\n    protected boolean mVideoCacheSwitch = false;\n\n    public PlayerImpl(Context context, PlayerAttributes attributes) {\n        applyPlayerAttr(attributes);\n        if (mVideoCacheSwitch) {\n            mLocalProxyPlayerImpl = new LocalProxyPlayerImpl(this);\n        }\n    }\n\n    protected void applyPlayerAttr(PlayerAttributes attributes) {\n        if (attributes == null) {\n            return;\n        }\n        mVideoCacheSwitch = attributes.videoCacheSwitch();\n        mUrl = attributes.getVideoUrl();\n    }\n\n    @Override\n    public void startLocalProxy(String url) {\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.startLocalProxy(url);\n        }\n    }\n\n    @Override\n    public void setOriginUrl(String url) {\n        mOriginUrl = url;\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.setCacheListener(mOriginUrl);\n        }\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n\n    }\n\n    @Override\n    public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException {\n\n    }\n\n    @Override\n    public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n\n    }\n\n    @Override\n    public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {\n\n    }\n\n    @Override\n    public void setSurface(Surface surface) {\n\n    }\n\n    @Override\n    public void setOnPreparedListener(OnPreparedListener listener) {\n        this.mOnPreparedListener = listener;\n    }\n\n    @Override\n    public void setOnVideoSizeChangedListener(OnVideoSizeChangedListener listener) {\n        this.mOnVideoSizeChangedListener = listener;\n    }\n\n    @Override\n    public void setOnErrorListener(OnErrorListener listener) {\n        this.mOnErrorListener = listener;\n    }\n\n    @Override\n    public void setOnLocalProxyCacheListener(OnLocalProxyCacheListener listener) {\n        this.mOnLocalProxyCacheListener = listener;\n    }\n\n    @Override\n    public void prepareAsync() throws IllegalStateException {\n    }\n\n    @Override\n    public void start() throws IllegalStateException {\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.doStartAction();\n        }\n    }\n\n    @Override\n    public void openPlay(PlayerAttributes attributes) {\n        applyPlayerAttr(attributes);\n        if (TextUtils.isEmpty(mUrl)) return;\n        if (mVideoCacheSwitch) {\n            mLocalProxyPlayerImpl.startLocalProxy(mUrl);\n        } else {\n            doOpenPlay(mUrl);\n        }\n    }\n\n    public abstract void doOpenPlay(String url);\n\n    @Override\n    public void pause() throws IllegalStateException {\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.doPauseAction();\n        }\n    }\n\n    @Override\n    public void setSpeed(float speed) {\n        \n    }\n\n    @Override\n    public void stop() throws IllegalStateException {\n\n    }\n\n    @Override\n    public void release() {\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.doReleaseAction();\n        }\n    }\n\n    @Override\n    public void seekTo(long msec) throws IllegalStateException {\n        if (mVideoCacheSwitch && mLocalProxyPlayerImpl != null) {\n            mLocalProxyPlayerImpl.doSeekToAction(msec);\n        }\n    }\n\n    @Override\n    public long getCurrentPosition() {\n        return 0;\n    }\n\n    @Override\n    public long getDuration() {\n        return 0;\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return false;\n    }\n\n    protected void notifyOnPrepared() {\n        if (mOnPreparedListener != null) {\n            mOnPreparedListener.onPrepared(this);\n        }\n    }\n\n    protected void notifyOnVideoSizeChanged(int width, int height,\n                                            int rotationDegree,\n                                            float pixelRatio,\n                                            float darRatio) {\n        if (mOnVideoSizeChangedListener != null) {\n            mOnVideoSizeChangedListener.onVideoSizeChanged(this, width, height, rotationDegree, pixelRatio, darRatio);\n        }\n    }\n\n    protected void notifyOnError(int what, String msg) {\n        if (mOnErrorListener != null) {\n            mOnErrorListener.onError(this, what, msg);\n        }\n    }\n\n    public void notifyProxyCacheReady(String proxyUrl) {\n        if (mOnLocalProxyCacheListener != null) {\n            mOnLocalProxyCacheListener.onCacheReady(this, proxyUrl);\n        }\n    }\n\n    public void notifyProxyCacheProgress(int percent, long cachedSize) {\n        if (mOnLocalProxyCacheListener != null) {\n            mOnLocalProxyCacheListener.onCacheProgressChanged(this, percent, cachedSize);\n        }\n    }\n\n    public void notifyProxyCacheSpeed(float speed) {\n        if (mOnLocalProxyCacheListener != null) {\n            mOnLocalProxyCacheListener.onCacheSpeedChanged(this, speed);\n        }\n    }\n\n    public void notifyProxyCacheForbidden(String url) {\n        if (mOnLocalProxyCacheListener != null) {\n            mOnLocalProxyCacheListener.onCacheForbidden(this, url);\n        }\n    }\n\n    public void notifyProxyCacheFinished() {\n        if (mOnLocalProxyCacheListener != null) {\n            mOnLocalProxyCacheListener.onCacheFinished(this);\n        }\n    }\n}\n"
  },
  {
    "path": "playersdk/src/main/java/com/android/player/proxy/LocalProxyPlayerImpl.java",
    "content": "package com.android.player.proxy;\n\nimport com.android.baselib.utils.LogUtils;\nimport com.android.player.impl.PlayerImpl;\nimport com.media.cache.VideoDownloadManager;\nimport com.media.cache.listener.IDownloadListener;\nimport com.media.cache.model.VideoTaskItem;\nimport com.media.cache.model.VideoTaskMode;\n\nimport java.lang.ref.WeakReference;\n\npublic class LocalProxyPlayerImpl {\n\n    private static final int NO_PAUSED = 0;\n    private static final int WIFI_PRELOAD_CONTROL = 1;\n    private static final int PLAYER_PAUSE_CONTROL = 2;\n    private static final int MOBILE_FLOW_CONTROL = 3;\n    private static final int PROXY_CACHE_EXCEPTION = 4;\n    private int mPausedReason = NO_PAUSED; // initial value or no paused state.\n\n    private WeakReference<PlayerImpl> mPlayer;\n    private boolean mUseLocalProxy = true;\n    private boolean mIsCompleteCached = false; //Video has been cached completely.\n    private String mUrl;\n    private boolean mVideoReady = false;\n    private int mCachedPercent = 0;\n    private long mCachedSize = 0L;\n    private VideoTaskItem mTaskItem;\n\n    public LocalProxyPlayerImpl(PlayerImpl player) {\n        mPlayer = new WeakReference<>(player);\n    }\n\n    public void startLocalProxy(String url) {\n        mUrl = url;\n        mTaskItem = new VideoTaskItem(url, VideoTaskMode.PLAY_MODE);\n        VideoDownloadManager.getInstance().startPlayCacheTask(mTaskItem, mDownloadListener);\n    }\n\n    public void setCacheListener(String url) {\n        mUrl = url;\n        VideoDownloadManager.getInstance().addCallback(url, mDownloadListener);\n    }\n\n    public void doStartAction() {\n        if (mUseLocalProxy) {\n            if (isProxyCacheTaskPaused()) {\n                resumeProxyCacheTask();\n            }\n        }\n    }\n\n    public void doSeekToAction(long seekPosition) {\n        if (mUseLocalProxy) {\n            if (mPlayer == null || mPlayer.get() == null)\n                return;\n            long totalDuration = mPlayer.get().getDuration();\n            if (totalDuration > 0) {\n                LogUtils.i(\"doSeekToAction seekPosition=\"+seekPosition);\n                mPausedReason = NO_PAUSED;\n                VideoDownloadManager.getInstance().seekToDownloadTask(seekPosition, totalDuration, mUrl);\n            }\n        }\n    }\n\n    public void doPauseAction() {\n        if (mUseLocalProxy) {\n            pauseProxyCacheTask(PLAYER_PAUSE_CONTROL);\n        }\n    }\n\n    public void doReleaseAction() {\n        if (mUseLocalProxy) {\n            LogUtils.i(\"doReleaseAction player=\"+this);\n            VideoDownloadManager.getInstance().stopDownloadTask(mTaskItem);\n        }\n    }\n\n    public void pauseProxyCacheTask(final int reason) {\n        if (mIsCompleteCached) {\n            VideoDownloadManager.getInstance().removeCallback(mUrl);\n            return;\n        }\n        //Do pauseProxyCacheTask when state is not paused.\n        if (!isProxyCacheTaskPaused()) {\n            mPausedReason = reason;\n            VideoDownloadManager.getInstance().pauseDownloadTask(mTaskItem);\n        }\n    }\n\n    public void resumeProxyCacheTask() {\n        if (mIsCompleteCached) {\n            return;\n        }\n        if (isProxyCacheTaskPaused()) {\n            LogUtils.i(\"resumeProxyCacheTask url=\"+mUrl);\n            mPausedReason = NO_PAUSED;\n            VideoDownloadManager.getInstance().resumeDownloadTask(mTaskItem, mDownloadListener);\n        }\n    }\n\n    private IDownloadListener mDownloadListener = new IDownloadListener() {\n\n        @Override\n        public void onDownloadDefault(VideoTaskItem item) { }\n\n        @Override\n        public void onDownloadPrepare(VideoTaskItem item) {\n            mTaskItem = item;\n        }\n\n        @Override\n        public void onDownloadPending(VideoTaskItem item) {\n            mTaskItem = item;\n        }\n\n        @Override\n        public void onDownloadStart(VideoTaskItem item) {\n            mTaskItem = item;\n        }\n\n        @Override\n        public void onDownloadProxyReady(VideoTaskItem item) {\n            mTaskItem = item;\n            if (!mVideoReady && mPlayer != null && mPlayer.get() != null) {\n                mPlayer.get().notifyProxyCacheReady(item.getProxyUrl());\n                mVideoReady = true;\n            }\n        }\n\n        @Override\n        public void onDownloadProgress(VideoTaskItem item) {\n            mTaskItem = item;\n            mCachedPercent = (int)item.getPercent();\n            mCachedSize = item.getDownloadSize();\n            if (mPlayer != null && mPlayer.get() != null) {\n                mPlayer.get().notifyProxyCacheProgress(mCachedPercent, mCachedSize);\n            }\n        }\n\n        @Override\n        public void onDownloadSpeed(VideoTaskItem item) {\n            mTaskItem = item;\n            if (mPlayer != null && mPlayer.get() != null) {\n                mPlayer.get().notifyProxyCacheSpeed(item.getSpeed());\n            }\n        }\n\n        @Override\n        public void onDownloadPause(VideoTaskItem item) {\n            mTaskItem = item;\n        }\n\n        @Override\n        public void onDownloadError(VideoTaskItem item) {\n            mTaskItem = item;\n            LogUtils.w(\"onDownloadError , player=\"+this);\n            pauseProxyCacheTask(PROXY_CACHE_EXCEPTION);\n        }\n\n        @Override\n        public void onDownloadProxyForbidden(VideoTaskItem item) {\n            mTaskItem = item;\n            LogUtils.w(\"onCacheForbidden url=\"+item.getUrl()+\", player=\"+this);\n            mUseLocalProxy = false;\n            if (mPlayer != null && mPlayer.get() != null) {\n                mPlayer.get().notifyProxyCacheForbidden(item.getUrl());\n            }\n        }\n\n        @Override\n        public void onDownloadSuccess(VideoTaskItem item) {\n            mTaskItem = item;\n            LogUtils.i(\"onDownloadSuccess url=\"+item.getUrl() + \", player=\"+this);\n            mIsCompleteCached = true;\n            if (mPlayer != null && mPlayer.get() != null) {\n                mPlayer.get().notifyProxyCacheFinished();\n            }\n        }\n    };\n\n    private boolean isProxyCacheTaskPaused() {\n        return mPausedReason != NO_PAUSED;\n    }\n\n}\n"
  },
  {
    "path": "playersdk/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">playerlib</string>\n</resources>\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':mediaproxy', ':exoplayer', ':playersdk', ':ijkplayer', ':base'\nrootProject.name='MediaSDK'\ninclude ':androidasync'\n"
  }
]